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 of output image timestamp modification in image processing #1509

Merged
merged 5 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 49 additions & 25 deletions cmd/image-processing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os"
"strconv"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/name"
containerreg "github.com/google/go-containerregistry/pkg/v1"
Expand All @@ -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
}

Comment on lines -48 to -67
Copy link
Contributor Author

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.

var flagValues settings

func initializeFlag() {
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -179,6 +201,8 @@ func runImageProcessing(ctx context.Context) error {
return err
}

log.Printf("Image %s@%s pushed\n", imageName.String(), digest)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Expand Down
5 changes: 5 additions & 0 deletions cmd/image-processing/main_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
)

func TestImageProcessingCmd(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Image Processing Command Suite")
}

func FailWith(substr string) types.GomegaMatcher {
return MatchError(ContainSubstring(substr))
}
111 changes: 105 additions & 6 deletions cmd/image-processing/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)))
Expand All @@ -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())
Expand Down Expand Up @@ -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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"))
})
})
})
Expand Down Expand Up @@ -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() {
Expand Down
Loading
Loading