container_images/registry-image-forked/commands/check.go (274 lines of code) (raw):

package commands import ( "encoding/json" "fmt" "io" "net/http" "sort" "strings" resource "github.com/GoogleCloudPlatform/guest-test-infra/container_images/registry-image-forked" "github.com/Masterminds/semver" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/sirupsen/logrus" ) // Check is type Check struct { stdin io.Reader stderr io.Writer stdout io.Writer args []string } // NewCheck is func NewCheck( stdin io.Reader, stderr io.Writer, stdout io.Writer, args []string, ) *Check { return &Check{ stdin: stdin, stderr: stderr, stdout: stdout, args: args, } } // Execute is func (c *Check) Execute() error { setupLogging(c.stderr) var req resource.CheckRequest decoder := json.NewDecoder(c.stdin) decoder.DisallowUnknownFields() err := decoder.Decode(&req) if err != nil { return fmt.Errorf("invalid payload: %s", err) } if req.Source.AwsAccessKeyID != "" && req.Source.AwsSecretAccessKey != "" && req.Source.AwsRegion != "" { if !req.Source.AuthenticateToECR() { return fmt.Errorf("cannot authenticate with ECR") } } mirrorSource, hasMirror, err := req.Source.Mirror() if err != nil { return fmt.Errorf("failed to resolve mirror: %w", err) } var response resource.CheckResponse if hasMirror { response, err = check(mirrorSource, req.Version) if err != nil { logrus.Warnf("checking mirror %s failed: %s", mirrorSource.Repository, err) } else if len(response) == 0 { logrus.Warnf("checking mirror %s failed: tag not found", mirrorSource.Repository) } } if len(response) == 0 { response, err = check(req.Source, req.Version) if err != nil { return fmt.Errorf("checking origin %s failed: %w", req.Source.Repository, err) } } err = json.NewEncoder(c.stdout).Encode(response) if err != nil { return fmt.Errorf("could not marshal JSON: %s", err) } return nil } func check(source resource.Source, from *resource.Version) (resource.CheckResponse, error) { repo, err := source.NewRepository() if err != nil { return resource.CheckResponse{}, fmt.Errorf("resolve repository: %w", err) } opts, err := source.AuthOptions(repo, []string{transport.PullScope}) if err != nil { return resource.CheckResponse{}, err } if source.Tag != "" { return checkTag(repo.Tag(source.Tag.String()), source, from, opts...) } return checkRepository(repo, source, from, opts...) } func checkRepository(repo name.Repository, source resource.Source, from *resource.Version, opts ...remote.Option) (resource.CheckResponse, error) { tags, err := remote.List(repo, opts...) if err != nil { return resource.CheckResponse{}, fmt.Errorf("list repository tags: %w", err) } bareTag := "latest" if source.Variant != "" { bareTag = source.Variant } versionTags := map[*semver.Version]name.Tag{} tagDigests := map[string]string{} digestVersions := map[string]*semver.Version{} var cursorVer *semver.Version var latestTag string if from != nil { // assess the 'from' tag first so we can skip lower version numbers sort.Slice(tags, func(i, j int) bool { return tags[i] == from.Tag }) } var constraint *semver.Constraints if source.SemverConstraint != "" { constraint, err = semver.NewConstraint(source.SemverConstraint) if err != nil { return resource.CheckResponse{}, fmt.Errorf("parse semver constraint: %w", err) } } for _, identifier := range tags { var ver *semver.Version if identifier == bareTag { latestTag = identifier } else { verStr := identifier if source.Variant != "" { if !strings.HasSuffix(identifier, "-"+source.Variant) { continue } verStr = strings.TrimSuffix(identifier, "-"+source.Variant) } ver, err = semver.NewVersion(verStr) if err != nil { // not a version continue } if constraint != nil && !constraint.Check(ver) { // semver constraint not met continue } pre := ver.Prerelease() if pre != "" { // pre-releases not enabled; skip if !source.PreReleases { continue } // contains additional variant if strings.Contains(pre, "-") { continue } if !strings.HasPrefix(pre, "alpha") && !strings.HasPrefix(pre, "beta") && !strings.HasPrefix(pre, "rc") { // additional variant, not a prerelease segment continue } } if cursorVer != nil && (cursorVer.GreaterThan(ver) || cursorVer.Equal(ver)) { // optimization: don't bother fetching digests for lesser (or equal but // less specific, i.e. 6.3 vs 6.3.0) version tags continue } } tagRef := repo.Tag(identifier) digest, found, err := headOrGet(tagRef, opts...) if err != nil { return resource.CheckResponse{}, fmt.Errorf("get tag digest: %w", err) } if !found { continue } tagDigests[identifier] = digest.String() if ver != nil { versionTags[ver] = tagRef existing, found := digestVersions[digest.String()] shouldSet := !found if found { if existing.Prerelease() == "" && ver.Prerelease() != "" { // favor final version over prereleases shouldSet = false } else if existing.Prerelease() != "" && ver.Prerelease() == "" { // favor final version over prereleases shouldSet = true } else if strings.Count(ver.Original(), ".") > strings.Count(existing.Original(), ".") { // favor more specific semver tag (i.e. 3.2.1 over 3.2, 1.0.0-rc.2 over 1.0.0-rc) shouldSet = true } } if shouldSet { digestVersions[digest.String()] = ver } } if from != nil && identifier == from.Tag && digest.String() == from.Digest { // if the 'from' version exists and has the same digest, treat its // version as a cursor in the tags, only considering newer versions // // note: the 'from' version will always be the first one hit by this loop cursorVer = ver } } var tagVersions TagVersions for digest, version := range digestVersions { tagVersions = append(tagVersions, TagVersion{ TagName: versionTags[version].TagStr(), Digest: digest, Version: version, }) } sort.Sort(tagVersions) response := resource.CheckResponse{} for _, ver := range tagVersions { response = append(response, resource.Version{ Tag: ver.TagName, Digest: ver.Digest, }) } if latestTag != "" { digest := tagDigests[latestTag] _, existsAsSemver := digestVersions[digest] if !existsAsSemver && constraint == nil { response = append(response, resource.Version{ Tag: latestTag, Digest: digest, }) } } return response, nil } // TagVersion is type TagVersion struct { TagName string Digest string Version *semver.Version } // TagVersions is type TagVersions []TagVersion // Len is func (vs TagVersions) Len() int { return len(vs) } // Less is func (vs TagVersions) Less(i, j int) bool { return vs[i].Version.LessThan(vs[j].Version) } // Swap is func (vs TagVersions) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } func checkTag(tag name.Tag, source resource.Source, version *resource.Version, opts ...remote.Option) (resource.CheckResponse, error) { digest, found, err := headOrGet(tag, opts...) if err != nil { return resource.CheckResponse{}, fmt.Errorf("get remote image: %w", err) } response := resource.CheckResponse{} if version != nil && found && version.Digest != digest.String() { digestRef := tag.Repository.Digest(version.Digest) _, found, err := headOrGet(digestRef, opts...) if err != nil { return resource.CheckResponse{}, fmt.Errorf("get remote image: %w", err) } if found { response = append(response, resource.Version{ Tag: tag.TagStr(), Digest: version.Digest, }) } } if found { response = append(response, resource.Version{ Tag: tag.TagStr(), Digest: digest.String(), }) } return response, nil } func headOrGet(ref name.Reference, imageOpts ...remote.Option) (v1.Hash, bool, error) { v1Desc, err := remote.Head(ref, imageOpts...) if err != nil { if checkMissingManifest(err) { return v1.Hash{}, false, nil } remoteDesc, err := remote.Get(ref, imageOpts...) if err != nil { if checkMissingManifest(err) { return v1.Hash{}, false, nil } if (remoteDesc.Digest == v1.Hash{}) { return v1.Hash{}, false, nil } return v1.Hash{}, false, err } return remoteDesc.Digest, true, nil } if (v1Desc.Digest == v1.Hash{}) { return v1.Hash{}, false, nil } return v1Desc.Digest, true, nil } func checkMissingManifest(err error) bool { if rErr, ok := err.(*transport.Error); ok { return rErr.StatusCode == http.StatusNotFound } return false }