Skip to content

Commit

Permalink
Add support for SOURCE_DATE_EPOCH
Browse files Browse the repository at this point in the history
When the `SOURCE_DATE_EPOCH` environment variable is set and the
`--ctime` parameter is not provided, the unix timestamp from
`SOURCE_DATE_EPOCH` will be used for setting the created time of the OCI
Tekton bundle image.
  • Loading branch information
zregvart committed Sep 28, 2023
1 parent 878f4c5 commit 5851a0e
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 4 deletions.
3 changes: 3 additions & 0 deletions docs/cmd/tkn_bundle_push.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Authentication:
Input:
Valid input in any form is valid Tekton YAML or JSON with a fully-specified "apiVersion" and "kind". To pass multiple objects in a single input, use "---" separators in YAML or a top-level "[]" in JSON.

Created time:
Setting created time of the OCI Image Configuration layer can be done by either providing it via --ctime parameter or setting the SOURCE_DATE_EPOCH environment variable.


### Options

Expand Down
4 changes: 4 additions & 0 deletions docs/man/man1/tkn-bundle-push.1
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Authentication:
Input:
Valid input in any form is valid Tekton YAML or JSON with a fully\-specified "apiVersion" and "kind". To pass multiple objects in a single input, use "\-\-\-" separators in YAML or a top\-level "[]" in JSON.

.PP
Created time:
Setting created time of the OCI Image Configuration layer can be done by either providing it via \-\-ctime parameter or setting the SOURCE\_DATE\_EPOCH environment variable.


.SH OPTIONS
.PP
Expand Down
27 changes: 24 additions & 3 deletions pkg/cmd/bundle/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
package bundle

import (
"errors"
"fmt"
"io"
"os"
"strconv"
"time"

"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -26,6 +28,8 @@ import (
"github.com/tektoncd/cli/pkg/params"
)

const sourceDateEpochEnv = "SOURCE_DATE_EPOCH"

type pushOptions struct {
cliparams cli.Params
stream *cli.Stream
Expand Down Expand Up @@ -56,6 +60,9 @@ Authentication:
Input:
Valid input in any form is valid Tekton YAML or JSON with a fully-specified "apiVersion" and "kind". To pass multiple objects in a single input, use "---" separators in YAML or a top-level "[]" in JSON.
Created time:
Setting created time of the OCI Image Configuration layer can be done by either providing it via --ctime parameter or setting the SOURCE_DATE_EPOCH environment variable.
`

c := &cobra.Command{
Expand Down Expand Up @@ -106,9 +113,12 @@ func (p *pushOptions) parseArgsAndFlags(args []string) (err error) {
if path == "-" {
// If this flag's value is '-', assume the user has piped input into stdin.
stdinContents, err := io.ReadAll(p.stream.In)
if err != nil || len(stdinContents) == 0 {
if err != nil {
return fmt.Errorf("failed to read bundle contents from stdin: %w", err)
}
if len(stdinContents) == 0 {
return errors.New("failed to read bundle contents from stdin: empty input")
}
p.bundleContents = append(p.bundleContents, string(stdinContents))
continue
}
Expand All @@ -124,7 +134,7 @@ func (p *pushOptions) parseArgsAndFlags(args []string) (err error) {
return err
}

if p.ctime, err = parseTime(p.ctimeParam); err != nil {
if p.ctime, err = determineCTime(p.ctimeParam); err != nil {
return err
}

Expand Down Expand Up @@ -153,7 +163,18 @@ func (p *pushOptions) Run(args []string) error {
// to help with testing
var now = time.Now

func parseTime(t string) (parsed time.Time, err error) {
func determineCTime(t string) (parsed time.Time, err error) {
// if given the parameter don't lookup the SOURCE_DATE_EPOCH env var
if t == "" {
if sourceDateEpoch, found := os.LookupEnv(sourceDateEpochEnv); found {
timestamp, err := strconv.ParseInt(sourceDateEpoch, 10, 64)
if err != nil {
return time.Time{}, err
}
return time.Unix(timestamp, 0), nil
}
}

if t == "" {
return now(), nil
}
Expand Down
118 changes: 117 additions & 1 deletion pkg/cmd/bundle/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func TestParseTime(t *testing.T) {

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, err := parseTime(c.given)
got, err := determineCTime(c.given)

if err != nil {
if err.Error() != c.err {
Expand Down Expand Up @@ -314,3 +314,119 @@ func TestPreRunE(t *testing.T) {
})
}
}

func TestParseArgsAndFlags(t *testing.T) {
cases := []struct {
name string
refArg string
bundleContent map[string]string
annotationsParams []string
ctimeParam string
sourceDateEpoch string
expectedRef string
expectedAnnotations map[string]string
expectedCTime time.Time
err string
}{
{
name: "default",
refArg: "registry.io/repository:tag",
bundleContent: map[string]string{"task1.yaml": "task1", "task2.yaml": "task2", "-": "stdin"},
annotationsParams: []string{"a=b", "c=d"},
expectedRef: "registry.io/repository:tag",
expectedAnnotations: map[string]string{"a": "b", "c": "d"},
expectedCTime: now(),
},
{
name: "ctime param",
refArg: "registry.io/repository:tag",
expectedRef: "registry.io/repository:tag",
ctimeParam: "1990-01-01",
expectedCTime: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
name: "SOURCE_DATE_EPOCH",
refArg: "registry.io/repository:tag",
sourceDateEpoch: "315529200",
expectedRef: "registry.io/repository:tag",
expectedCTime: time.Unix(315529200, 0),
},
{
name: "empty stdin",
refArg: "registry.io/repository:tag",
bundleContent: map[string]string{"-": ""},
expectedRef: "registry.io/repository:tag",
expectedCTime: now(),
err: "failed to read bundle contents from stdin: empty input",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
dir := t.TempDir()

expectedContent := make([]string, 0, len(c.bundleContent))

opts := pushOptions{
bundleContentPaths: make([]string, 0, len(c.bundleContent)),
annotationParams: c.annotationsParams,
ctimeParam: c.ctimeParam,
}

if c.sourceDateEpoch != "" {
t.Setenv(sourceDateEpochEnv, c.sourceDateEpoch)
}

for p, c := range c.bundleContent {
name := p
content := []byte(c)
if name == "-" {
opts.stream = &cli.Stream{
In: bytes.NewBuffer(content),
}
} else {
name = path.Join(dir, p)
if err := os.WriteFile(name, content, 0o400); err != nil {
t.Fatalf("unable to write test file: %s", err)
}
}

opts.bundleContentPaths = append(opts.bundleContentPaths, name)
expectedContent = append(expectedContent, c)
}

if err := opts.parseArgsAndFlags([]string{c.refArg}); err != nil {
if err.Error() != c.err {
t.Errorf("unexpected error, expecting %q, got: %q", c.err, err)
}

// no need to test any further
return
} else if c.err != "" {
t.Errorf("expected an error %q", c.err)
}

if expected, got := c.expectedRef, opts.ref.String(); expected != got {
t.Errorf("expected parsed reference to be %q, but it was %q", expected, got)
}

if expected, got := len(c.bundleContent), len(opts.bundleContents); expected != got {
t.Errorf("expected %d files to be read for the bundle, but it was %d", expected, got)
}

for i, expected := range expectedContent {
if opts.bundleContents[i] != expected {
t.Errorf("bundle content at %d (%q) is not as expected", i, opts.bundleContentPaths[i])
}
}

if expected, got := fmt.Sprint(c.expectedAnnotations), fmt.Sprint(opts.annotations); expected != got {
t.Errorf("expected annotations %q differ from parsed: %q", expected, got)
}

if expected, got := c.expectedCTime, opts.ctime; expected.Unix() != got.Unix() {
t.Errorf("expected ctime %s differs from parsed: %s", expected, got)
}
})
}
}

0 comments on commit 5851a0e

Please sign in to comment.