container_images/registry-image-forked/commands/out.go (275 lines of code) (raw):
package commands
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
resource "github.com/GoogleCloudPlatform/guest-test-infra/container_images/registry-image-forked"
"github.com/Masterminds/semver"
"github.com/google/go-containerregistry/pkg/authn"
"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/google/go-containerregistry/pkg/v1/tarball"
"github.com/simonshyu/notary-gcr/pkg/gcr"
"github.com/sirupsen/logrus"
)
// Out is
type Out struct {
stdin io.Reader
stderr io.Writer
stdout io.Writer
args []string
}
// NewOut is
func NewOut(
stdin io.Reader,
stderr io.Writer,
stdout io.Writer,
args []string,
) *Out {
return &Out{
stdin: stdin,
stderr: stderr,
stdout: stdout,
args: args,
}
}
// Execute is
func (o *Out) Execute() error {
setupLogging(o.stderr)
var req resource.OutRequest
decoder := json.NewDecoder(o.stdin)
decoder.DisallowUnknownFields()
err := decoder.Decode(&req)
if err != nil {
return fmt.Errorf("invalid payload: %s", err)
}
if req.Source.Debug {
logrus.SetLevel(logrus.DebugLevel)
}
if len(o.args) < 2 {
return fmt.Errorf("destination path not specified")
}
src := o.args[1]
if req.Source.AwsAccessKeyID != "" && req.Source.AwsSecretAccessKey != "" && req.Source.AwsRegion != "" {
if !req.Source.AuthenticateToECR() {
return fmt.Errorf("cannot authenticate with ECR")
}
}
tagsToPush := []name.Tag{}
repo, err := req.Source.NewRepository()
if err != nil {
return fmt.Errorf("could not resolve repository: %w", err)
}
if req.Source.Tag != "" {
tagsToPush = append(tagsToPush, repo.Tag(req.Source.Tag.String()))
}
if req.Params.Version != "" {
ver, err := semver.NewVersion(req.Params.Version)
if err != nil {
if err == semver.ErrInvalidSemVer {
return fmt.Errorf("invalid semantic version: %q", req.Params.Version)
}
return fmt.Errorf("failed to parse version: %w", err)
}
// vito: subtle gotcha here - if someone passes the version as v1.2.3, the
// 'v' will be stripped, as *semver.Version parses it but does not preserve
// it in .String().
//
// we could call .Original(), of course, but it seems common practice to
// *not* have the v prefix in Docker image tags, so it might be better to
// just enforce it until someone complains enough; it seems more likely to
// be an accident than a legacy practice that must be preserved.
//
// if that's the person reading this: sorry! PR welcome! (maybe we should
// add tag_prefix:?)
tag := ver.String()
if req.Source.Variant != "" {
tag += "-" + req.Source.Variant
}
tagsToPush = append(tagsToPush, repo.Tag(tag))
if req.Params.BumpAliases && ver.Prerelease() == "" {
aliasTags, err := aliasesToBump(req, repo, ver)
if err != nil {
return fmt.Errorf("determine aliases: %w", err)
}
tagsToPush = append(tagsToPush, aliasTags...)
}
}
additionalTags, err := req.Params.ParseAdditionalTags(src)
if err != nil {
return fmt.Errorf("could not parse additional tags: %w", err)
}
for _, tagName := range additionalTags {
tag, err := name.NewTag(fmt.Sprintf("%s:%s", req.Source.Repository, tagName))
if err != nil {
return fmt.Errorf("could not resolve repository/tag reference: %w", err)
}
tagsToPush = append(tagsToPush, tag)
}
if len(tagsToPush) == 0 {
return fmt.Errorf("no tag specified - need either 'version:' in params or 'tag:' in source")
}
imagePath := filepath.Join(src, req.Params.Image)
matches, err := filepath.Glob(imagePath)
if err != nil {
return fmt.Errorf("failed to glob path '%s': %w", req.Params.Image, err)
}
if len(matches) == 0 {
return fmt.Errorf("no files match glob '%s'", req.Params.Image)
}
if len(matches) > 1 {
return fmt.Errorf("too many files match glob '%s': %v", req.Params.Image, matches)
}
img, err := tarball.ImageFromPath(matches[0], nil)
if err != nil {
return fmt.Errorf("could not load image from path '%s': %w", req.Params.Image, err)
}
digest, err := img.Digest()
if err != nil {
return fmt.Errorf("failed to get image digest: %w", err)
}
err = resource.RetryOnRateLimit(func() error {
return put(req, img, tagsToPush)
})
if err != nil {
return fmt.Errorf("pushing image failed: %w", err)
}
pushedTags := []string{}
for _, tag := range tagsToPush {
pushedTags = append(pushedTags, tag.TagStr())
}
err = json.NewEncoder(os.Stdout).Encode(resource.OutResponse{
Version: resource.Version{
Tag: tagsToPush[0].TagStr(),
Digest: digest.String(),
},
Metadata: append(req.Source.Metadata(), resource.MetadataField{
Name: "tags",
Value: strings.Join(pushedTags, " "),
}),
})
if err != nil {
return fmt.Errorf("could not marshal JSON: %s", err)
}
return nil
}
func put(req resource.OutRequest, img v1.Image, tags []name.Tag) error {
images := map[name.Reference]remote.Taggable{}
var identifiers []string
for _, tag := range tags {
images[tag] = img
identifiers = append(identifiers, tag.Identifier())
}
repo, err := req.Source.NewRepository()
if err != nil {
return fmt.Errorf("resolve repository name: %w", err)
}
opts, err := req.Source.AuthOptions(repo, []string{transport.PushScope})
if err != nil {
return err
}
logrus.Infof("pushing tag(s) %s", strings.Join(identifiers, ", "))
err = remote.MultiWrite(images, opts...)
if err != nil {
return fmt.Errorf("pushing tag(s): %w", err)
}
logrus.Info("pushed")
if req.Source.ContentTrust != nil {
err = signImages(req, img, tags)
if err != nil {
return fmt.Errorf("signing image(s): %w", err)
}
}
return nil
}
func signImages(req resource.OutRequest, img v1.Image, tags []name.Tag) error {
var notaryConfigDir string
var err error
notaryConfigDir, err = req.Source.ContentTrust.PrepareConfigDir()
if err != nil {
return fmt.Errorf("prepare notary-config-dir: %w", err)
}
for _, tag := range tags {
trustedRepo, err := gcr.NewTrustedGcrRepository(notaryConfigDir, tag, createAuth(req))
if err != nil {
return fmt.Errorf("create TrustedGcrRepository: %w", err)
}
logrus.Infof("signing image with tag: %s", tag.Identifier())
err = trustedRepo.SignImage(img)
if err != nil {
logrus.Errorf("failed to sign image: %s", err)
}
}
return nil
}
// It's okay if both are blank. It will become an Anonymous Authenticator in
// that case.
func createAuth(req resource.OutRequest) *authn.Basic {
return &authn.Basic{
Username: req.Source.Username,
Password: req.Source.Password,
}
}
func aliasesToBump(req resource.OutRequest, repo name.Repository, ver *semver.Version) ([]name.Tag, error) {
variant := req.Source.Variant
repo, err := req.Source.NewRepository()
if err != nil {
return nil, fmt.Errorf("resolve repository name: %w", err)
}
opts, err := req.Source.AuthOptions(repo, []string{transport.PullScope})
if err != nil {
return nil, err
}
versions, err := remote.List(repo, opts...)
if err != nil && !isNewImage(err) {
return nil, fmt.Errorf("list repository tags: %w", err)
}
aliases := []name.Tag{}
bumpLatest := true
bumpMajor := true
bumpMinor := true
for _, v := range versions {
versionStr := v
if variant != "" {
if !strings.HasSuffix(versionStr, "-"+variant) {
// don't compare across variants
continue
}
versionStr = strings.TrimSuffix(versionStr, "-"+variant)
}
remoteVer, err := semver.NewVersion(versionStr)
if err != nil {
continue
}
// don't compare to prereleases or other variants
if remoteVer.Prerelease() != "" {
continue
}
if remoteVer.GreaterThan(ver) {
bumpLatest = false
}
if remoteVer.Major() == ver.Major() && remoteVer.Minor() > ver.Minor() {
bumpMajor = false
}
if remoteVer.Major() == ver.Major() && remoteVer.Minor() == ver.Minor() && remoteVer.Patch() > ver.Patch() {
bumpMinor = false
bumpMajor = false
}
}
if bumpLatest {
latestTag := "latest"
if variant != "" {
latestTag = variant
}
aliases = append(aliases, repo.Tag(latestTag))
}
if bumpMajor {
tagName := fmt.Sprintf("%d", ver.Major())
if variant != "" {
tagName += "-" + variant
}
aliases = append(aliases, repo.Tag(tagName))
}
if bumpMinor {
tagName := fmt.Sprintf("%d.%d", ver.Major(), ver.Minor())
if variant != "" {
tagName += "-" + variant
}
aliases = append(aliases, repo.Tag(tagName))
}
return aliases, nil
}
func isNewImage(err error) bool {
if e, ok := err.(*transport.Error); ok && e.StatusCode == http.StatusNotFound {
return e.Errors[0].Code == transport.NameUnknownErrorCode || e.Errors[0].Code == "NOT_FOUND"
}
return false
}