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
16 changes: 16 additions & 0 deletions pkg/imgpkg/bundle/contents.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ func (b Contents) Push(uploadRef regname.Tag, labels map[string]string, registry
return plainimage.NewContents(b.paths, b.excludedPaths, b.preservePermissions).Push(uploadRef, labels, registry, logger)
}

// MultiTagPush pushes the OCI Bundle to the registry with multiple tags
func (b Contents) MultiTagPush(uploadRefs []regname.Tag, labels map[string]string, registry ImagesMetadataWriter, logger Logger) (string, error) {
phenixblue marked this conversation as resolved.
Show resolved Hide resolved

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
err := b.validate()
if err != nil {
return "", err
}

if labels == nil {
labels = map[string]string{}
}
labels[BundleConfigLabel] = "true"

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

// PresentsAsBundle checks if the provided folders have the needed structure to be a bundle
func (b Contents) PresentsAsBundle() (bool, error) {
imgpkgDirs, err := b.findImgpkgDirs()
Expand Down
153 changes: 139 additions & 14 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,21 +95,60 @@ 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)

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
imageURL := ""
imageRefs := []string{}
uploadRefs := []regname.Tag{}

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 = append(uploadRefs, baseRef)
phenixblue marked this conversation as resolved.
Show resolved Hide resolved

// TODO(phenixblue): Cleanup when done testing
fmt.Printf("\nAdding base ref to list of tags: %s\n", baseRef)
phenixblue marked this conversation as resolved.
Show resolved Hide resolved

// Loop through all tags specified by the user and push the related image+tag
for _, tag := range po.TagFlags.Tags {

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
uploadRef, err := regname.NewTag(baseImageName+":"+tag, regname.WeakValidation)
if err != nil {
return "", fmt.Errorf("Parsing '%s': %s", tag, err)
}

uploadRefs = append(uploadRefs, uploadRef)
// TODO(phenixblue): Cleanup when done testing
fmt.Printf("\nAdding non-base ref to list of tags: %s\n", uploadRef)
phenixblue marked this conversation as resolved.
Show resolved Hide resolved

}

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)
if err != nil {
return "", err

if len(po.TagFlags.Tags) > 1 {
imageURL, err = bundle.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).MultiTagPush(uploadRefs, po.LabelFlags.Labels, registry, logger)
if err != nil {
return "", err
}

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

if po.LockOutputFlags.LockFilePath != "" {
Expand All @@ -116,8 +158,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 +170,25 @@ 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) {

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
imageURL := ""
imageRefs := []string{}
uploadRefs := []regname.Tag{}

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 +197,52 @@ 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 = append(uploadRefs, baseRef)
phenixblue marked this conversation as resolved.
Show resolved Hide resolved

// Loop through all tags specified by the user and push the related image+tag
for _, tag := range po.TagFlags.Tags {

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
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)

if len(po.TagFlags.Tags) > 1 {
imageURL, err = plainimage.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).MultiTagPush(uploadRefs, po.LabelFlags.Labels, registry, logger)
if err != nil {
return "", err
}
} else {
imageURL, err = plainimage.NewContents(po.FileFlags.Files, po.FileFlags.ExcludedFilePaths, po.FileFlags.PreservePermissions).Push(uploadRefs[0], 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 +258,35 @@ func (po *PushOptions) validateFlags() error {
return nil

}

func (po *PushOptions) stripTag() (string, error) {

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
object := ""
isBundle := po.BundleFlags.Bundle != ""
isImage := po.ImageFlags.Image != ""

switch {

phenixblue marked this conversation as resolved.
Show resolved Hide resolved
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