Skip to content

Commit

Permalink
Add --image-timestamp flag to image-processing
Browse files Browse the repository at this point in the history
Add command-line flag `--image-timestamp` to update the image creation
timestamp of the image when using shipwright-managed push.
  • Loading branch information
HeavyWombat committed Mar 1, 2024
1 parent 9e3c444 commit 5705c65
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 6 deletions.
44 changes: 44 additions & 0 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 @@ -40,6 +41,8 @@ type settings struct {
label []string
insecure bool
image,
imageTimestamp,
imageTimestampFile,
resultFileImageDigest,
resultFileImageSize,
secretPath string
Expand All @@ -61,6 +64,10 @@ func initializeFlag() {

pflag.StringArrayVar(&flagValues.annotation, "annotation", nil, "New annotations to add")
pflag.StringArrayVar(&flagValues.label, "label", nil, "New labels to add")

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

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

// 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",
)).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",
"--image", tag.String(),
"--annotation", "org.opencontainers.image.url*https://my-company.com/images",
)).To(HaveOccurred())
)).To(FailWith("not enough parts"))
})
})

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

0 comments on commit 5705c65

Please sign in to comment.