image lookup: do not match *any* tags

For reasons buried in the history of Podman, looking up an untagged
image would match any tag of matching image. For instance, looking up
centos would match a local image centos:foobar.  Change that behavior
to only match the latest tag.

Fix: #11964
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2021-11-22 13:48:14 +01:00
parent a55473bea8
commit 0d1aaf080e
35 changed files with 593 additions and 246 deletions

View File

@ -624,8 +624,7 @@ func (i *Image) NamedRepoTags() ([]reference.Named, error) {
}
// inRepoTags looks for the specified name/tag pair in the image's repo tags.
// Note that tag may be empty.
func (i *Image) inRepoTags(name, tag string) (reference.Named, error) {
func (i *Image) inRepoTags(namedTagged reference.NamedTagged) (reference.Named, error) {
repoTags, err := i.NamedRepoTags()
if err != nil {
return nil, err
@ -636,8 +635,10 @@ func (i *Image) inRepoTags(name, tag string) (reference.Named, error) {
return nil, err
}
name := namedTagged.Name()
tag := namedTagged.Tag()
for _, pair := range pairs {
if tag != "" && tag != pair.Tag {
if tag != pair.Tag {
continue
}
if !strings.HasSuffix(pair.Name, name) {

View File

@ -389,16 +389,17 @@ func (r *Runtime) lookupImageInDigestsAndRepoTags(name string, options *LookupIm
return nil, "", err
}
if !shortnames.IsShortName(name) {
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, "", err
}
digested, hasDigest := named.(reference.Digested)
if !hasDigest {
return nil, "", errors.Wrap(storage.ErrImageUnknown, name)
}
ref, err := reference.Parse(name) // Warning! This is not ParseNormalizedNamed
if err != nil {
return nil, "", err
}
named, isNamed := ref.(reference.Named)
if !isNamed {
return nil, "", errors.Wrap(storage.ErrImageUnknown, name)
}
digested, isDigested := named.(reference.Digested)
if isDigested {
logrus.Debug("Looking for image with matching recorded digests")
digest := digested.Digest()
for _, image := range allImages {
@ -408,22 +409,23 @@ func (r *Runtime) lookupImageInDigestsAndRepoTags(name string, options *LookupIm
}
}
}
return nil, "", errors.Wrap(storage.ErrImageUnknown, name)
}
// Podman compat: if we're looking for a short name but couldn't
// resolve it via the registries.conf dance, we need to look at *all*
// images and check if the name we're looking for matches a repo tag.
// Split the name into a repo/tag pair
split := strings.SplitN(name, ":", 2)
repo := split[0]
tag := ""
if len(split) == 2 {
tag = split[1]
if !shortnames.IsShortName(name) {
return nil, "", errors.Wrap(storage.ErrImageUnknown, name)
}
named = reference.TagNameOnly(named) // Make sure to add ":latest" if needed
namedTagged, isNammedTagged := named.(reference.NamedTagged)
if !isNammedTagged {
// NOTE: this should never happen since we already know it's
// not a digested reference.
return nil, "", fmt.Errorf("%s: %w (could not cast to tagged)", name, storage.ErrImageUnknown)
}
for _, image := range allImages {
named, err := image.inRepoTags(repo, tag)
named, err := image.inRepoTags(namedTagged)
if err != nil {
return nil, "", err
}

View File

@ -215,6 +215,12 @@ type EngineConfig struct {
// The first path pointing to a valid file will be used.
ConmonPath []string `toml:"conmon_path,omitempty"`
// CompatAPIEnforceDockerHub enforces using docker.io for completing
// short names in Podman's compatibility REST API. Note that this will
// ignore unqualified-search-registries and short-name aliases defined
// in containers-registries.conf(5).
CompatAPIEnforceDockerHub bool `toml:"compat_api_enforce_docker_hub,omitempty"`
// DetachKeys is the sequence of keys used to detach a container.
DetachKeys string `toml:"detach_keys,omitempty"`

View File

@ -317,6 +317,11 @@ default_sysctls = [
# "/usr/local/sbin/conmon"
#]
# Enforces using docker.io for completing short names in Podman's compatibility
# REST API. Note that this will ignore unqualified-search-registries and
# short-name aliases defined in containers-registries.conf(5).
#compat_api_enforce_docker_hub = true
# Specify the keys sequence used to detach a container.
# Format is a single character [a-Z] or a comma separated sequence of
# `ctrl-<value>`, where `<value>` is one of:

View File

@ -190,6 +190,7 @@ func DefaultConfig() (*Config, error) {
IPCNS: "private",
LogDriver: defaultLogDriver(),
LogSizeMax: DefaultLogSizeMax,
NetNS: "private",
NoHosts: false,
PidsLimit: DefaultPidsLimit,
PidNS: "private",
@ -225,7 +226,7 @@ func defaultSecretConfig() SecretConfig {
func defaultMachineConfig() MachineConfig {
return MachineConfig{
CPUs: 1,
DiskSize: 10,
DiskSize: 100,
Image: "testing",
Memory: 2048,
}
@ -243,6 +244,8 @@ func defaultConfigFromMemory() (*EngineConfig, error) {
c.EventsLogFilePath = filepath.Join(c.TmpDir, "events", "events.log")
c.CompatAPIEnforceDockerHub = true
if path, ok := os.LookupEnv("CONTAINERS_STORAGE_CONF"); ok {
types.SetDefaultConfigFilePath(path)
}

View File

@ -3,34 +3,44 @@ Package report provides helper structs/methods/funcs for formatting output
To format output for an array of structs:
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
ExamplePodman:
headers := report.Headers(struct {
ID string
}{}, nil)
t, _ := report.NewTemplate("command name").Parse("{{range .}}{{.ID}}{{end}}")
t.Execute(t, headers)
t.Execute(t, map[string]string{
f := report.New(os.Stdout, "Command Name")
f, _ := f.Parse(report.OriginPodman, "{{range .}}{{.ID}}{{end}}")
defer f.Flush()
if f.RenderHeaders {
f.Execute(headers)
}
f.Execute( map[string]string{
"ID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == false
or
w := report.NewWriterDefault(os.Stdout)
defer w.Flush()
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
ExampleUser:
headers := report.Headers(struct {
CID string
}{}, map[string]string{
"CID":"ID"})
t, _ := report.NewTemplate("command name").Parse("table {{.CID}}")
t.Execute(t, headers)
}{}, map[string]string{"CID":"ID"})
f, _ := report.New(os.Stdout, "Command Name").Parse(report.OriginUser, "table {{.CID}}")
defer f.Flush()
if f.RenderHeaders {
t.Execute(t, headers)
}
t.Execute(t,map[string]string{
"CID":"fa85da03b40141899f3af3de6d27852b",
})
// t.IsTable() == true
// Output:
// ID
// fa85da03b40141899f3af3de6d27852b
Helpers:
@ -38,13 +48,20 @@ Helpers:
... process JSON and output
}
if report.HasTable(cmd.Flag("format").Value.String()) {
... "table" keyword prefix in format text
}
Template Functions:
The following template functions are added to the template when parsed:
- join strings.Join, {{join .Field separator}}
- json encode field as JSON {{ json .Field }}
- lower strings.ToLower {{ .Field | lower }}
- pad add spaces as prefix and suffix {{ pad . 2 2 }}
- split strings.Split {{ .Field | split }}
- title strings.Title {{ .Field | title }}
- truncate limit field length {{ truncate . 10 }}
- upper strings.ToUpper {{ .Field | upper }}
report.Funcs() may be used to add additional template functions.

View File

@ -0,0 +1,151 @@
package report
import (
"io"
"strings"
"text/tabwriter"
"text/template"
)
// Flusher is the interface that wraps the Flush method.
type Flusher interface {
Flush() error
}
// NopFlusher represents a type which flush operation is nop.
type NopFlusher struct{}
// Flush is a nop operation.
func (f *NopFlusher) Flush() (err error) { return }
type Origin int
const (
OriginUnknown Origin = iota
OriginPodman
OriginUser
)
func (o Origin) String() string {
switch o {
case OriginPodman:
return "OriginPodman"
case OriginUser:
return "OriginUser"
default:
return "OriginUnknown"
}
}
// Formatter holds the configured Writer and parsed Template, additional state fields are
// maintained to assist in the podman command report writing.
type Formatter struct {
Origin Origin // Source of go template. OriginUser or OriginPodman
RenderHeaders bool // Hint, default behavior for given template is to include headers
RenderTable bool // Does template have "table" keyword
flusher Flusher // Flush any buffered formatted output
template *template.Template // Go text/template for formatting output
text string // value of canonical template after processing
writer io.Writer // Destination for formatted output
}
// Parse parses golang template returning a formatter
//
// - OriginPodman implies text is a template from podman code. Output will
// be filtered through a tabwriter.
//
// - OriginUser implies text is a template from a user. If template includes
// keyword "table" output will be filtered through a tabwriter.
func (f *Formatter) Parse(origin Origin, text string) (*Formatter, error) {
f.Origin = origin
switch {
case strings.HasPrefix(text, "table "):
f.RenderTable = true
text = "{{range .}}" + NormalizeFormat(text) + "{{end -}}"
case OriginUser == origin:
text = EnforceRange(NormalizeFormat(text))
default:
text = NormalizeFormat(text)
}
f.text = text
if f.RenderTable || origin == OriginPodman {
tw := tabwriter.NewWriter(f.writer, 12, 2, 2, ' ', tabwriter.StripEscape)
f.writer = tw
f.flusher = tw
f.RenderHeaders = true
}
tmpl, err := f.template.Funcs(template.FuncMap(DefaultFuncs)).Parse(text)
if err != nil {
return f, err
}
f.template = tmpl
return f, nil
}
// Funcs adds the elements of the argument map to the template's function map.
// A default template function will be replaced if there is a key collision.
func (f *Formatter) Funcs(funcMap template.FuncMap) *Formatter {
m := make(template.FuncMap, len(DefaultFuncs)+len(funcMap))
for k, v := range DefaultFuncs {
m[k] = v
}
for k, v := range funcMap {
m[k] = v
}
f.template = f.template.Funcs(funcMap)
return f
}
// Init either resets the given tabwriter with new values or wraps w in tabwriter with given values
func (f *Formatter) Init(w io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Formatter {
flags |= tabwriter.StripEscape
if tw, ok := f.writer.(*tabwriter.Writer); ok {
tw = tw.Init(w, minwidth, tabwidth, padding, padchar, flags)
f.writer = tw
f.flusher = tw
} else {
tw = tabwriter.NewWriter(w, minwidth, tabwidth, padding, padchar, flags)
f.writer = tw
f.flusher = tw
}
return f
}
// Execute applies a parsed template to the specified data object,
// and writes the output to Formatter.Writer.
func (f *Formatter) Execute(data interface{}) error {
return f.template.Execute(f.writer, data)
}
// Flush should be called after the last call to Write to ensure
// that any data buffered in the Formatter is written to output. Any
// incomplete escape sequence at the end is considered
// complete for formatting purposes.
func (f *Formatter) Flush() error {
// Indirection is required here to prevent caller from having to know when
// value of Flusher may be changed.
return f.flusher.Flush()
}
// Writer returns the embedded io.Writer from Formatter
func (f *Formatter) Writer() io.Writer {
return f.writer
}
// New allocates a new, undefined Formatter with the given name and Writer
func New(output io.Writer, name string) *Formatter {
f := new(Formatter)
f.flusher = new(NopFlusher)
if flusher, ok := output.(Flusher); ok {
f.flusher = flusher
}
f.template = template.New(name)
f.writer = output
return f
}

View File

@ -25,17 +25,19 @@ var tableReplacer = strings.NewReplacer(
"table ", "",
`\t`, "\t",
" ", "\t",
`\n`, "\n",
)
// escapedReplacer will clean up escaped characters from CLI
var escapedReplacer = strings.NewReplacer(
`\t`, "\t",
`\n`, "\n",
)
var DefaultFuncs = FuncMap{
"join": strings.Join,
"json": func(v interface{}) string {
buf := &bytes.Buffer{}
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.Encode(v)
@ -157,7 +159,7 @@ func (t *Template) IsTable() bool {
return t.isTable
}
var rangeRegex = regexp.MustCompile(`{{\s*range\s*\.\s*}}.*{{\s*end\s*-?\s*}}`)
var rangeRegex = regexp.MustCompile(`(?s){{\s*range\s*\.\s*}}.*{{\s*end\s*-?\s*}}`)
// EnforceRange ensures that the format string contains a range
func EnforceRange(format string) string {