-
Notifications
You must be signed in to change notification settings - Fork 113
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 of output image timestamp modification in image processing #1509
Changes from all commits
7dbe18f
f4ffa2e
d77aec5
f5bfbcb
237c8dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import ( | |
"os" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/google/go-containerregistry/pkg/name" | ||
containerreg "github.com/google/go-containerregistry/pkg/v1" | ||
|
@@ -37,34 +38,16 @@ type settings struct { | |
help bool | ||
push string | ||
annotation, | ||
label *[]string | ||
label []string | ||
insecure bool | ||
image, | ||
imageTimestamp, | ||
imageTimestampFile, | ||
resultFileImageDigest, | ||
resultFileImageSize, | ||
secretPath string | ||
} | ||
|
||
func getAnnotation() []string { | ||
var annotation []string | ||
|
||
if flagValues.annotation != nil { | ||
return append(annotation, *flagValues.annotation...) | ||
} | ||
|
||
return annotation | ||
} | ||
|
||
func getLabel() []string { | ||
var label []string | ||
|
||
if flagValues.label != nil { | ||
return append(label, *flagValues.label...) | ||
} | ||
|
||
return label | ||
} | ||
|
||
var flagValues settings | ||
|
||
func initializeFlag() { | ||
|
@@ -79,8 +62,12 @@ func initializeFlag() { | |
|
||
pflag.StringVar(&flagValues.push, "push", "", "Push the image contained in this directory") | ||
|
||
flagValues.annotation = pflag.StringArray("annotation", nil, "New annotations to add") | ||
flagValues.label = pflag.StringArray("label", nil, "New labels to add") | ||
pflag.StringArrayVar(&flagValues.annotation, "annotation", nil, "New annotations to add") | ||
pflag.StringArrayVar(&flagValues.label, "label", nil, "New labels to add") | ||
Comment on lines
+65
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to unify the style with the other flags |
||
|
||
pflag.StringVar(&flagValues.imageTimestamp, "image-timestamp", "", "number to use as Unix timestamp to set image creation timestamp") | ||
pflag.StringVar(&flagValues.imageTimestampFile, "image-timestamp-file", "", "path to a file containing a unix timestamp to set as the image timestamp") | ||
|
||
pflag.StringVar(&flagValues.resultFileImageDigest, "result-file-image-digest", "", "A file to write the image digest to") | ||
pflag.StringVar(&flagValues.resultFileImageSize, "result-file-image-size", "", "A file to write the image size to") | ||
} | ||
|
@@ -109,6 +96,27 @@ func Execute(ctx context.Context) error { | |
return nil | ||
} | ||
|
||
// validate that only one of the image timestamp flags are used | ||
if flagValues.imageTimestamp != "" && flagValues.imageTimestampFile != "" { | ||
pflag.Usage() | ||
return fmt.Errorf("image timestamp and image timestamp file flag is used, they are mutually exclusive, only use one") | ||
} | ||
|
||
// validate that image timestamp file exists (if set), and translate it into the imageTimestamp field | ||
if flagValues.imageTimestampFile != "" { | ||
_, err := os.Stat(flagValues.imageTimestampFile) | ||
if err != nil { | ||
return fmt.Errorf("image timestamp file flag references a non-existing file: %w", err) | ||
} | ||
|
||
data, err := os.ReadFile(flagValues.imageTimestampFile) | ||
if err != nil { | ||
return fmt.Errorf("failed to read image timestamp from %s: %w", flagValues.imageTimestampFile, err) | ||
} | ||
|
||
flagValues.imageTimestamp = string(data) | ||
} | ||
|
||
return runImageProcessing(ctx) | ||
} | ||
|
||
|
@@ -123,13 +131,13 @@ func runImageProcessing(ctx context.Context) error { | |
} | ||
|
||
// parse annotations | ||
annotations, err := splitKeyVals(getAnnotation()) | ||
annotations, err := splitKeyVals(flagValues.annotation) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// parse labels | ||
labels, err := splitKeyVals(getLabel()) | ||
labels, err := splitKeyVals(flagValues.label) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -171,6 +179,20 @@ func runImageProcessing(ctx context.Context) error { | |
} | ||
} | ||
|
||
// mutate the image timestamp | ||
if flagValues.imageTimestamp != "" { | ||
sec, err := strconv.ParseInt(flagValues.imageTimestamp, 10, 32) | ||
if err != nil { | ||
return fmt.Errorf("failed to parse image timestamp value %q as a number: %w", flagValues.imageTimestamp, err) | ||
} | ||
|
||
log.Println("Mutating the image timestamp") | ||
img, imageIndex, err = image.MutateImageOrImageIndexTimestamp(img, imageIndex, time.Unix(sec, 0)) | ||
if err != nil { | ||
return fmt.Errorf("failed to mutate the timestamp: %w", err) | ||
} | ||
} | ||
|
||
// push the image and determine the digest and size | ||
log.Printf("Pushing the image to registry %q\n", imageName.String()) | ||
digest, size, err := image.PushImageOrImageIndex(imageName, img, imageIndex, options) | ||
|
@@ -179,6 +201,8 @@ func runImageProcessing(ctx context.Context) error { | |
return err | ||
} | ||
|
||
log.Printf("Image %s@%s pushed\n", imageName.String(), digest) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only to allow for better time tracking in the user logs. |
||
|
||
// Writing image digest to file | ||
if digest != "" && flagValues.resultFileImageDigest != "" { | ||
if err := os.WriteFile(flagValues.resultFileImageDigest, []byte(digest), 0400); err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,13 @@ import ( | |
"net/url" | ||
"os" | ||
"strconv" | ||
"time" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
. "github.com/shipwright-io/build/cmd/image-processing" | ||
|
||
"github.com/google/go-containerregistry/pkg/crane" | ||
"github.com/google/go-containerregistry/pkg/name" | ||
"github.com/google/go-containerregistry/pkg/registry" | ||
containerreg "github.com/google/go-containerregistry/pkg/v1" | ||
|
@@ -67,6 +69,14 @@ var _ = Describe("Image Processing Resource", func() { | |
f(u.Host) | ||
} | ||
|
||
withTempDir := func(f func(target string)) { | ||
path, err := os.MkdirTemp(os.TempDir(), "temp-dir") | ||
Expect(err).ToNot(HaveOccurred()) | ||
defer os.RemoveAll(path) | ||
|
||
f(path) | ||
} | ||
|
||
withTestImage := func(f func(tag name.Tag)) { | ||
withTempRegistry(func(endpoint string) { | ||
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5))) | ||
|
@@ -77,6 +87,19 @@ var _ = Describe("Image Processing Resource", func() { | |
}) | ||
} | ||
|
||
withTestImageAsDirectory := func(f func(path string, tag name.Tag)) { | ||
withTempRegistry(func(endpoint string) { | ||
withTempDir(func(dir string) { | ||
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5))) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
Expect(crane.SaveOCI(empty.Image, dir)).To(Succeed()) | ||
|
||
f(dir, tag) | ||
}) | ||
}) | ||
} | ||
|
||
getCompressedImageSize := func(img containerreg.Image) int64 { | ||
manifest, err := img.Manifest() | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
@@ -157,34 +180,70 @@ var _ = Describe("Image Processing Resource", func() { | |
}) | ||
|
||
It("should fail in case mandatory arguments are missing", func() { | ||
Expect(run()).To(HaveOccurred()) | ||
Expect(run()).ToNot(Succeed()) | ||
}) | ||
|
||
It("should fail in case --image is empty", func() { | ||
Expect(run("--image", "")).To(HaveOccurred()) | ||
Expect(run( | ||
"--image", "", | ||
)).To(FailWith("argument must not be empty")) | ||
}) | ||
|
||
It("should fail in case --image does not exist", func() { | ||
Expect(run( | ||
"--image", "docker.io/feqlQoDIHc/bcfHFHHXYF", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name in the use case was actually invalid. |
||
)).To(HaveOccurred()) | ||
"--image", "docker.io/library/feqlqodihc:bcfhfhhxyf", | ||
)).To(FailWith("unexpected status code 401")) | ||
}) | ||
|
||
It("should fail in case annotation is invalid", func() { | ||
withTestImage(func(tag name.Tag) { | ||
Expect(run( | ||
"--insecure", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a bug in the test cases actually, the test case did not test what was intended to test. |
||
"--image", tag.String(), | ||
"--annotation", "org.opencontainers.image.url*https://my-company.com/images", | ||
)).To(HaveOccurred()) | ||
)).To(FailWith("not enough parts")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switched to error text matching in order to avoid not checking for that actual problem in the tests. |
||
}) | ||
}) | ||
|
||
It("should fail in case label is invalid", func() { | ||
withTestImage(func(tag name.Tag) { | ||
Expect(run( | ||
"--insecure", | ||
"--image", tag.String(), | ||
"--label", " description*image description", | ||
)).To(HaveOccurred()) | ||
)).To(FailWith("not enough parts")) | ||
}) | ||
}) | ||
|
||
It("should fail if both --image-timestamp and --image-timestamp-file are used", func() { | ||
Expect(run( | ||
"--image-timestamp", "1234567890", | ||
"--image-timestamp-file", "/tmp/foobar", | ||
)).To(FailWith("image timestamp and image timestamp file flag is used")) | ||
}) | ||
|
||
It("should fail if --image-timestamp-file is used with a non-existing file", func() { | ||
Expect("/tmp/does-not-exist").ToNot(BeAnExistingFile()) | ||
Expect(run( | ||
"--image-timestamp-file", "/tmp/does-not-exist", | ||
)).To(FailWith("image timestamp file flag references a non-existing file")) | ||
}) | ||
|
||
It("should fail if --image-timestamp-file referenced file cannot be used", func() { | ||
withTempDir(func(wrong string) { | ||
Expect(run( | ||
"--image-timestamp-file", wrong, | ||
)).To(FailWith("failed to read image timestamp from")) | ||
}) | ||
}) | ||
|
||
It("should fail in case timestamp is invalid", func() { | ||
withTestImage(func(tag name.Tag) { | ||
Expect(run( | ||
"--insecure", | ||
"--image", tag.String(), | ||
"--image-timestamp", "foobar", | ||
)).To(FailWith("failed to parse image timestamp")) | ||
}) | ||
}) | ||
}) | ||
|
@@ -266,6 +325,46 @@ var _ = Describe("Image Processing Resource", func() { | |
To(Equal("https://my-company.com/images")) | ||
}) | ||
}) | ||
|
||
It("should mutate the image timestamp using a provided timestamp", func() { | ||
withTestImageAsDirectory(func(path string, tag name.Tag) { | ||
Expect(run( | ||
"--insecure", | ||
"--push", path, | ||
"--image", tag.String(), | ||
"--image-timestamp", "1234567890", | ||
)).ToNot(HaveOccurred()) | ||
|
||
image := getImage(tag) | ||
|
||
cfgFile, err := image.ConfigFile() | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
Expect(cfgFile.Created.Time).To(BeTemporally("==", time.Unix(1234567890, 0))) | ||
}) | ||
}) | ||
|
||
It("should mutate the image timestamp using a provided timestamp in a file", func() { | ||
withTestImageAsDirectory(func(path string, tag name.Tag) { | ||
withTempFile("timestamp", func(filename string) { | ||
Expect(os.WriteFile(filename, []byte("1234567890"), os.FileMode(0644))) | ||
|
||
Expect(run( | ||
"--insecure", | ||
"--push", path, | ||
"--image", tag.String(), | ||
"--image-timestamp-file", filename, | ||
)).ToNot(HaveOccurred()) | ||
|
||
image := getImage(tag) | ||
|
||
cfgFile, err := image.ConfigFile() | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
Expect(cfgFile.Created.Time).To(BeTemporally("==", time.Unix(1234567890, 0))) | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
Context("store result after image mutation", func() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplifying the flag usage meant that these became obsolete.