Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to handle additional OCI tags from user input #553

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
6 changes: 3 additions & 3 deletions pkg/imgpkg/bundle/contents.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func NewContents(paths []string, excludedPaths []string, preservePermissions boo
return Contents{paths: paths, excludedPaths: excludedPaths, preservePermissions: preservePermissions}
}

// Push the contents of the bundle to the registry as an OCI Image
func (b Contents) Push(uploadRef regname.Tag, labels map[string]string, registry ImagesMetadataWriter, logger Logger) (string, error) {
// Push the contents of the bundle to the registry as an OCI Image with one or more tags
func (b Contents) Push(uploadRefs []regname.Tag, labels map[string]string, registry ImagesMetadataWriter, logger Logger) (string, error) {
err := b.validate()
if err != nil {
return "", err
Expand All @@ -54,7 +54,7 @@ func (b Contents) Push(uploadRef regname.Tag, labels map[string]string, registry
}
labels[BundleConfigLabel] = "true"

return plainimage.NewContents(b.paths, b.excludedPaths, b.preservePermissions).Push(uploadRef, labels, registry, logger)
return plainimage.NewContents(b.paths, b.excludedPaths, b.preservePermissions).Push(uploadRefs, labels, registry, logger)
}

// PresentsAsBundle checks if the provided folders have the needed structure to be a bundle
Expand Down
4 changes: 2 additions & 2 deletions pkg/imgpkg/bundle/contents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ images:
t.Fatalf("failed to read tag: %s", err)
}

_, err = subject.Push(imgTag, map[string]string{}, fakeRegistry, util.NewNoopLevelLogger())
_, err = subject.Push([]name.Tag{imgTag}, map[string]string{}, fakeRegistry, util.NewNoopLevelLogger())
if err != nil {
t.Fatalf("not expecting push to fail: %s", err)
}
Expand Down Expand Up @@ -78,7 +78,7 @@ images:
t.Fatalf("failed to read tag: %s", err)
}

_, err = subject.Push(imgTag, map[string]string{}, fakeRegistry, util.NewNoopLevelLogger())
_, err = subject.Push([]name.Tag{imgTag}, map[string]string{}, fakeRegistry, util.NewNoopLevelLogger())
if err != nil {
t.Fatalf("not expecting push to fail: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/imgpkg/bundle/locations_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (r LocationsConfigs) Save(reg ImagesMetadataWriter, bundleRef name.Digest,

r.ui.Tracef("Pushing image\n")

_, err = plainimage.NewContents([]string{tmpDir}, nil, false).Push(locRef, nil, reg.CloneWithLogger(util.NewNoopProgressBar()), logger)
_, err = plainimage.NewContents([]string{tmpDir}, nil, false).Push([]name.Tag{locRef}, nil, reg.CloneWithLogger(util.NewNoopProgressBar()), logger)
if err != nil {
// Immutable tag errors within registries are not standardized.
// Assume word "immutable" would be present in most cases.
Expand Down
120 changes: 108 additions & 12 deletions pkg/imgpkg/cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd

import (
"fmt"
"strings"

"github.com/cppforlife/go-cli-ui/ui"
regname "github.com/google/go-containerregistry/pkg/name"
Expand All @@ -25,6 +26,7 @@ type PushOptions struct {
FileFlags FileFlags
RegistryFlags RegistryFlags
LabelFlags LabelFlags
TagFlags TagFlags
}

func NewPushOptions(ui ui.UI) *PushOptions {
Expand All @@ -49,6 +51,7 @@ func NewPushCmd(o *PushOptions) *cobra.Command {
o.FileFlags.Set(cmd)
o.RegistryFlags.Set(cmd)
o.LabelFlags.Set(cmd)
o.TagFlags.Set(cmd)

return cmd
}
Expand Down Expand Up @@ -92,19 +95,41 @@ func (po *PushOptions) Run() error {
panic("Unreachable code")
}

po.ui.BeginLinef("Pushed '%s'", imageURL)
po.ui.BeginLinef("\nPushed: \n%s\n", imageURL)

return nil
}

func (po *PushOptions) pushBundle(registry registry.Registry) (string, error) {
uploadRef, err := regname.NewTag(po.BundleFlags.Bundle, regname.WeakValidation)
imageURL := ""
imageRefs := []string{}

baseImageName, err := po.stripTag()
if err != nil {
return "", err
}

baseRef, err := regname.NewTag(po.BundleFlags.Bundle, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", po.BundleFlags.Bundle, err)
}

// Append the base image_tag to the list of refs to upload
uploadRefs := []regname.Tag{baseRef}

// Loop through all tags specified by the user and push the related image+tag
for _, tag := range po.TagFlags.Tags {
uploadRef, err := regname.NewTag(baseImageName+":"+tag, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", tag, err)
}

uploadRefs = append(uploadRefs, uploadRef)
}

logger := util.NewUILevelLogger(util.LogWarn, util.NewLogger(po.ui))
imageURL, err := bundle.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).Push(uploadRef, po.LabelFlags.Labels, registry, logger)

imageURL, err = bundle.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).Push(uploadRefs, po.LabelFlags.Labels, registry, logger)
if err != nil {
return "", err
}
Expand All @@ -116,8 +141,9 @@ func (po *PushOptions) pushBundle(registry registry.Registry) (string, error) {
Kind: lockconfig.BundleLockKind,
},
Bundle: lockconfig.BundleRef{
Image: imageURL,
Tag: uploadRef.TagStr(),
Image: imageURL,
Tag: uploadRefs[0].TagStr(),
OtherTags: strings.Join(po.TagFlags.Tags, ","),
},
}

Expand All @@ -127,19 +153,23 @@ func (po *PushOptions) pushBundle(registry registry.Registry) (string, error) {
}
}

return imageURL, nil
if !strings.Contains(strings.Join(imageRefs, ","), imageURL) {
imageRefs = append(imageRefs, imageURL)
}

po.ui.BeginLinef("\nTags: %s, %s\n", baseRef.TagStr(), strings.Join(po.TagFlags.Tags, ", "))

return strings.Join(imageRefs, "\n"), nil
}

func (po *PushOptions) pushImage(registry registry.Registry) (string, error) {
imageURL := ""
imageRefs := []string{}

if po.LockOutputFlags.LockFilePath != "" {
return "", fmt.Errorf("Lock output is not compatible with image, use bundle for lock output")
}

uploadRef, err := regname.NewTag(po.ImageFlags.Image, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", po.ImageFlags.Image, err)
}

isBundle, err := bundle.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).PresentsAsBundle()
if err != nil {
return "", err
Expand All @@ -148,8 +178,43 @@ func (po *PushOptions) pushImage(registry registry.Registry) (string, error) {
return "", fmt.Errorf("Images cannot be pushed with '.imgpkg' directories, consider using --bundle (-b) option")
}

baseImageName, err := po.stripTag()
if err != nil {
return "", err
}

baseRef, err := regname.NewTag(po.ImageFlags.Image, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", po.BundleFlags.Bundle, err)
}

// Append the base image_tag to the list of refs to upload
uploadRefs := []regname.Tag{baseRef}

// Loop through all tags specified by the user and push the related image+tag
for _, tag := range po.TagFlags.Tags {
uploadRef, err := regname.NewTag(baseImageName+":"+tag, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", tag, err)
}

uploadRefs = append(uploadRefs, uploadRef)
}

logger := util.NewUILevelLogger(util.LogWarn, util.NewLogger(po.ui))
return plainimage.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).Push(uploadRef, po.LabelFlags.Labels, registry, logger)

imageURL, err = plainimage.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).Push(uploadRefs, po.LabelFlags.Labels, registry, logger)
if err != nil {
return "", err
}

if !strings.Contains(strings.Join(imageRefs, ","), imageURL) {
imageRefs = append(imageRefs, imageURL)
}

po.ui.BeginLinef("\nTags: %s, %s\n", baseRef.TagStr(), strings.Join(po.TagFlags.Tags, ", "))

return strings.Join(imageRefs, "\n"), nil
}

// validateFlags checks if the provided flags are valid
Expand All @@ -165,3 +230,34 @@ func (po *PushOptions) validateFlags() error {
return nil

}

// stripTag removes the tag from the provided image or bundle reference
func (po *PushOptions) stripTag() (string, error) {
object := ""
isBundle := po.BundleFlags.Bundle != ""
isImage := po.ImageFlags.Image != ""

switch {
case isBundle:
object = po.BundleFlags.Bundle

case isImage:
object = po.ImageFlags.Image

default:
panic("Unreachable code")
}

objectRef, err := regname.NewTag(object, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", object, err)
}

baseObjectName := strings.TrimSuffix(objectRef.Name(), ":"+objectRef.TagStr())

if baseObjectName == "" {
return "", fmt.Errorf("'%s' is not a valid image reference", object)
}

return baseObjectName, nil
}
112 changes: 112 additions & 0 deletions pkg/imgpkg/cmd/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,118 @@ func TestLabels(t *testing.T) {
}
}

func TestTags(t *testing.T) {
testCases := []struct {
name string
opType string
expectedError string
expectedTags []string
tagInput string
inlineTag string
}{
{
name: "bundle with one inline tag",
opType: "bundle",
expectedError: "",
tagInput: "",
expectedTags: []string{"v1.0.1"},
inlineTag: "v1.0.1",
},
{
name: "bundle with one tag via flag",
opType: "bundle",
expectedError: "",
tagInput: "v1.0.1",
expectedTags: []string{"v1.0.1", "latest"},
inlineTag: "",
},
{
name: "bundle with inline tag and tag via flag",
opType: "bundle",
expectedError: "",
tagInput: "v1.2.0-alpha,latest",
expectedTags: []string{"v1.0.1", "v1.2.0-alpha", "latest"},
inlineTag: "v1.0.1",
},
{
name: "bundle with multiple tags via flag",
opType: "bundle",
expectedError: "",
tagInput: "v1.0.1,v1.0.2",
expectedTags: []string{"v1.0.1", "v1.0.2", "latest"},
inlineTag: "",
},
{
name: "image with one inline tag",
opType: "image",
expectedError: "",
tagInput: "",
expectedTags: []string{"v1.0.1"},
inlineTag: "v1.0.1",
},
{
name: "image with one tag via flag",
opType: "image",
expectedError: "",
tagInput: "v1.0.1",
expectedTags: []string{"v1.0.1", "latest"},
inlineTag: "",
},
{
name: "image with inline tag and tags via flag",
opType: "image",
expectedError: "",
tagInput: "latest,stable",
expectedTags: []string{"v1.0.1", "latest"},
inlineTag: "v1.0.1",
},
}

for _, tc := range testCases {
f := func(t *testing.T) {
env := helpers.BuildEnv(t)
targetImage := env.Image
imgpkg := helpers.Imgpkg{T: t, ImgpkgPath: env.ImgpkgPath}
defer env.Cleanup()

opTypeFlag := "-b"
pushDir := env.BundleFactory.CreateBundleDir(helpers.BundleYAML, helpers.ImagesYAML)

if tc.opType == "image" {
opTypeFlag = "-i"
pushDir = env.Assets.CreateAndCopySimpleApp("image-to-push")
}

if tc.inlineTag != "" {
targetImage = env.Image + ":" + tc.inlineTag
}

if tc.tagInput == "" {
imgpkg.Run([]string{"push", opTypeFlag, targetImage, "-f", pushDir})
} else {
imgpkg.Run([]string{"push", opTypeFlag, targetImage, "--additional-tags", tc.tagInput, "-f", pushDir})
}

// Loop through expected tags and validate they exist on the image
for _, tag := range tc.expectedTags {
ref, _ := name.NewTag(env.Image+":"+tag, name.WeakValidation)
image, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
require.NoError(t, err)

tagList := imgpkg.Run([]string{"tag", "ls", "-i", env.Image + ":" + tag})

_, err = image.ConfigFile()
require.NoError(t, err)

require.Contains(t, tagList, tag, "Expected tags provided via flags to match tags discovered for image")

}
}

t.Run(tc.name, f)
}
}

func Cleanup(dirs ...string) {
for _, dir := range dirs {
os.RemoveAll(dir)
Expand Down
18 changes: 18 additions & 0 deletions pkg/imgpkg/cmd/tag_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"github.com/spf13/cobra"
)

// TagFlags is a struct that holds the additional tags for an OCI artifact
type TagFlags struct {
Tags []string
}

// Set sets additional tags for an OCI artifact
func (t *TagFlags) Set(cmd *cobra.Command) {
cmd.Flags().StringSliceVar(&t.Tags, "additional-tags", []string{}, "Set additional tags on image")
}
5 changes: 3 additions & 2 deletions pkg/imgpkg/lockconfig/bundle_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ type BundleLock struct {
}

type BundleRef struct {
Image string `json:"image,omitempty"` // This generated yaml, but due to lib we need to use `json`
Tag string `json:"tag,omitempty"` // This generated yaml, but due to lib we need to use `json`
Image string `json:"image,omitempty"` // This generated yaml, but due to lib we need to use `json`
Tag string `json:"tag,omitempty"` // This generated yaml, but due to lib we need to use `json`
OtherTags string `json:"otherTags,omitempty"`
}

func NewBundleLockFromPath(path string) (BundleLock, error) {
Expand Down
Loading
Loading