package image import ( "strings" "time" "github.com/distribution/distribution/v3/reference" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" ) type ContainerImage struct { RegistryURL string ImageName string ImageTag *tag.ImageTag ImageAlias string HelmParamImageName string HelmParamImageVersion string KustomizeImage *ContainerImage original string } type ContainerImageList []*ContainerImage // NewFromIdentifier parses an image identifier and returns a populated ContainerImage func NewFromIdentifier(identifier string) *ContainerImage { imgRef := identifier alias := "" if strings.Contains(identifier, "=") { n := strings.SplitN(identifier, "=", 2) imgRef = n[1] alias = n[0] } if parsed, err := reference.ParseNormalizedNamed(imgRef); err == nil { img := ContainerImage{} img.RegistryURL = reference.Domain(parsed) // remove default registry for backwards-compatibility if img.RegistryURL == "docker.io" && !strings.HasPrefix(imgRef, "docker.io") { img.RegistryURL = "" } img.ImageAlias = alias img.ImageName = reference.Path(parsed) // if library/ was added to the image name, remove it if !strings.HasPrefix(imgRef, "library/") { img.ImageName = strings.TrimPrefix(img.ImageName, "library/") } if digested, ok := parsed.(reference.Digested); ok { img.ImageTag = &tag.ImageTag{ TagDigest: string(digested.Digest()), } } else if tagged, ok := parsed.(reference.Tagged); ok { img.ImageTag = &tag.ImageTag{ TagName: tagged.Tag(), } } img.original = identifier return &img } // if distribution couldn't parse it, fall back to the legacy parsing logic img := ContainerImage{} img.RegistryURL = getRegistryFromIdentifier(identifier) img.ImageAlias, img.ImageName, img.ImageTag = getImageTagFromIdentifier(identifier) img.original = identifier return &img } // String returns the string representation of given ContainerImage func (img *ContainerImage) String() string { str := "" if img.ImageAlias != "" { str += img.ImageAlias str += "=" } str += img.GetFullNameWithTag() return str } func (img *ContainerImage) GetFullNameWithoutTag() string { str := "" if img.RegistryURL != "" { str += img.RegistryURL + "/" } str += img.ImageName return str } // GetFullNameWithTag returns the complete image slug, including the registry // and any tag digest or tag name set for the image. func (img *ContainerImage) GetFullNameWithTag() string { str := "" if img.RegistryURL != "" { str += img.RegistryURL + "/" } str += img.ImageName if img.ImageTag != nil { if img.ImageTag.TagName != "" { str += ":" str += img.ImageTag.TagName } if img.ImageTag.TagDigest != "" { str += "@" str += img.ImageTag.TagDigest } } return str } // GetTagWithDigest returns tag name along with any tag digest set for the image func (img *ContainerImage) GetTagWithDigest() string { str := "" if img.ImageTag != nil { if img.ImageTag.TagName != "" { str += img.ImageTag.TagName } if img.ImageTag.TagDigest != "" { if str == "" { str += "latest" } str += "@" str += img.ImageTag.TagDigest } } return str } func (img *ContainerImage) Original() string { return img.original } // IsUpdatable checks whether the given image can be updated with newTag while // taking tagSpec into account. tagSpec must be given as a semver compatible // version spec, i.e. ^1.0 or ~2.1 func (img *ContainerImage) IsUpdatable(newTag, tagSpec string) bool { return false } // WithTag returns a copy of img with new tag information set func (img *ContainerImage) WithTag(newTag *tag.ImageTag) *ContainerImage { nimg := &ContainerImage{} nimg.RegistryURL = img.RegistryURL nimg.ImageName = img.ImageName nimg.ImageTag = newTag nimg.ImageAlias = img.ImageAlias nimg.HelmParamImageName = img.HelmParamImageName nimg.HelmParamImageVersion = img.HelmParamImageVersion return nimg } func (img *ContainerImage) DiffersFrom(other *ContainerImage, checkVersion bool) bool { return img.RegistryURL != other.RegistryURL || img.ImageName != other.ImageName || (checkVersion && img.ImageTag.TagName != other.ImageTag.TagName) } // ContainsImage checks whether img is contained in a list of images func (list *ContainerImageList) ContainsImage(img *ContainerImage, checkVersion bool) *ContainerImage { // if there is a KustomizeImage override, check it for a match first if img.KustomizeImage != nil { if kustomizeMatch := list.ContainsImage(img.KustomizeImage, checkVersion); kustomizeMatch != nil { return kustomizeMatch } } for _, image := range *list { if img.ImageName == image.ImageName && image.RegistryURL == img.RegistryURL { if !checkVersion || image.ImageTag.TagName == img.ImageTag.TagName { return image } } } return nil } func (list *ContainerImageList) Originals() []string { results := make([]string, len(*list)) for i, img := range *list { results[i] = img.Original() } return results } // String Returns the name of all images as a string, separated using comma func (list *ContainerImageList) String() string { imgNameList := make([]string, 0) for _, image := range *list { imgNameList = append(imgNameList, image.String()) } return strings.Join(imgNameList, ",") } // Gets the registry URL from an image identifier func getRegistryFromIdentifier(identifier string) string { var imageString string comp := strings.Split(identifier, "=") if len(comp) > 1 { imageString = comp[1] } else { imageString = identifier } comp = strings.Split(imageString, "/") if len(comp) > 1 && strings.Contains(comp[0], ".") { return comp[0] } else { return "" } } // Gets the image name and tag from an image identifier func getImageTagFromIdentifier(identifier string) (string, string, *tag.ImageTag) { var imageString string var sourceName string // The original name is prepended to the image name, separated by = comp := strings.SplitN(identifier, "=", 2) if len(comp) == 2 { sourceName = comp[0] imageString = comp[1] } else { imageString = identifier } // Strip any repository identifier from the string comp = strings.Split(imageString, "/") if len(comp) > 1 && strings.Contains(comp[0], ".") { imageString = strings.Join(comp[1:], "/") } // We can either have a tag name or a digest reference, or both // jannfis/test-image:0.1 // gcr.io/jannfis/test-image:0.1 // gcr.io/jannfis/test-image@sha256:abcde // gcr.io/jannfis/test-image:test-tag@sha256:abcde if strings.Contains(imageString, "@") { comp = strings.SplitN(imageString, "@", 2) colonPos := strings.LastIndex(comp[0], ":") slashPos := strings.LastIndex(comp[0], "/") if colonPos > slashPos { // first half (before @) contains image and tag name return sourceName, comp[0][:colonPos], tag.NewImageTag(comp[0][colonPos+1:], time.Unix(0, 0), comp[1]) } else { // first half contains image name without tag name return sourceName, comp[0], tag.NewImageTag("", time.Unix(0, 0), comp[1]) } } else { comp = strings.SplitN(imageString, ":", 2) if len(comp) != 2 { return sourceName, imageString, nil } else { tagName, tagDigest := getImageDigestFromTag(comp[1]) return sourceName, comp[0], tag.NewImageTag(tagName, time.Unix(0, 0), tagDigest) } } } func getImageDigestFromTag(tagStr string) (string, string) { a := strings.Split(tagStr, "@") if len(a) != 2 { return tagStr, "" } else { return a[0], a[1] } } // LogContext returns a log context for the given image, with required fields // set to the image's information. func (img *ContainerImage) LogContext() *log.LogContext { logCtx := log.WithContext() logCtx.AddField("image_name", img.GetFullNameWithoutTag()) logCtx.AddField("image_alias", img.ImageAlias) logCtx.AddField("registry_url", img.RegistryURL) if img.ImageTag != nil { logCtx.AddField("image_tag", img.ImageTag.TagName) logCtx.AddField("image_digest", img.ImageTag.TagDigest) } return logCtx }