Skip to content

Commit ae280b3

Browse files
Merge pull request #1471 from shipwright-io/add/source-timestamp
Add source timestamp field for source result
2 parents 717bfb7 + f181f4c commit ae280b3

27 files changed

+467
-141
lines changed

.github/workflows/ci.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: Unit, Integration, and E2E Tests
2-
on:
2+
on:
33
pull_request:
44
branches:
55
- main
66
push:
77
paths-ignore:
88
- 'README.md'
99
- 'docs/**'
10-
branches:
10+
branches:
1111
- main
1212

1313
jobs:
@@ -114,8 +114,9 @@ jobs:
114114
# host.docker.internal does not work in a GitHub action
115115
docker exec kind-control-plane bash -c "echo '172.17.0.1 host.docker.internal' >>/etc/hosts"
116116
117-
# Build and load the Git image
117+
# Build and load the Git and Bundle image
118118
export GIT_CONTAINER_IMAGE="$(KO_DOCKER_REPO=kind.local ko publish ./cmd/git)"
119+
export BUNDLE_CONTAINER_IMAGE="$(KO_DOCKER_REPO=kind.local ko publish ./cmd/bundle)"
119120
120121
make test-integration
121122

cmd/bundle/main.go

+36-10
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,25 @@ import (
99
"fmt"
1010
"log"
1111
"os"
12+
"strconv"
1213

1314
"github.com/google/go-containerregistry/pkg/name"
15+
"github.com/google/go-containerregistry/pkg/v1/mutate"
16+
"github.com/google/go-containerregistry/pkg/v1/remote"
1417
"github.com/spf13/pflag"
1518

1619
"github.com/shipwright-io/build/pkg/bundle"
1720
"github.com/shipwright-io/build/pkg/image"
1821
)
1922

2023
type settings struct {
21-
help bool
22-
image string
23-
prune bool
24-
target string
25-
secretPath string
26-
resultFileImageDigest string
24+
help bool
25+
image string
26+
prune bool
27+
target string
28+
secretPath string
29+
resultFileImageDigest string
30+
resultFileSourceTimestamp string
2731
}
2832

2933
var flagValues settings
@@ -36,6 +40,7 @@ func init() {
3640
pflag.StringVar(&flagValues.image, "image", "", "Location of the bundle image (mandatory)")
3741
pflag.StringVar(&flagValues.target, "target", "/workspace/source", "The target directory to place the code")
3842
pflag.StringVar(&flagValues.resultFileImageDigest, "result-file-image-digest", "", "A file to write the image digest")
43+
pflag.StringVar(&flagValues.resultFileSourceTimestamp, "result-file-source-timestamp", "", "A file to write the source timestamp")
3944

4045
pflag.StringVar(&flagValues.secretPath, "secret-path", "", "A directory that contains access credentials (optional)")
4146
pflag.BoolVar(&flagValues.prune, "prune", false, "Delete bundle image from registry after it was pulled")
@@ -72,10 +77,20 @@ func Do(ctx context.Context) error {
7277
}
7378

7479
log.Printf("Pulling image %q", ref)
75-
img, err := bundle.PullAndUnpack(
76-
ref,
77-
flagValues.target,
78-
options...)
80+
desc, err := remote.Get(ref, options...)
81+
if err != nil {
82+
return err
83+
}
84+
85+
img, err := desc.Image()
86+
if err != nil {
87+
return err
88+
}
89+
90+
rc := mutate.Extract(img)
91+
defer rc.Close()
92+
93+
unpackDetails, err := bundle.Unpack(rc, flagValues.target)
7994
if err != nil {
8095
return err
8196
}
@@ -93,6 +108,17 @@ func Do(ctx context.Context) error {
93108
}
94109
}
95110

111+
if flagValues.resultFileSourceTimestamp != "" {
112+
if unpackDetails.MostRecentFileTimestamp != nil {
113+
if err = os.WriteFile(flagValues.resultFileSourceTimestamp, []byte(strconv.FormatInt(unpackDetails.MostRecentFileTimestamp.Unix(), 10)), 0644); err != nil {
114+
return err
115+
}
116+
117+
} else {
118+
log.Printf("Unable to determine source timestamp of content in %s\n", flagValues.target)
119+
}
120+
}
121+
96122
if flagValues.prune {
97123
// Some container registry implementations, i.e. library/registry:2 will fail to
98124
// delete the image when there is no image digest given. Use image digest from the

cmd/bundle/main_test.go

+93-9
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,31 @@ import (
99
"fmt"
1010
"io"
1111
"log"
12+
"net/http/httptest"
13+
"net/url"
1214
"os"
1315
"path/filepath"
16+
"time"
1417

1518
. "github.com/onsi/ginkgo/v2"
1619
. "github.com/onsi/gomega"
1720

1821
. "github.com/shipwright-io/build/cmd/bundle"
19-
"github.com/shipwright-io/build/pkg/image"
2022

2123
"github.com/google/go-containerregistry/pkg/name"
24+
"github.com/google/go-containerregistry/pkg/registry"
2225
containerreg "github.com/google/go-containerregistry/pkg/v1"
2326
"github.com/google/go-containerregistry/pkg/v1/remote"
2427
"k8s.io/apimachinery/pkg/util/rand"
28+
29+
"github.com/shipwright-io/build/pkg/bundle"
30+
"github.com/shipwright-io/build/pkg/image"
2531
)
2632

2733
var _ = Describe("Bundle Loader", func() {
2834
const exampleImage = "ghcr.io/shipwright-io/sample-go/source-bundle:latest"
2935

30-
var run = func(args ...string) error {
36+
run := func(args ...string) error {
3137
// discard log output
3238
log.SetOutput(io.Discard)
3339

@@ -40,7 +46,7 @@ var _ = Describe("Bundle Loader", func() {
4046
return Do(context.Background())
4147
}
4248

43-
var withTempDir = func(f func(target string)) {
49+
withTempDir := func(f func(target string)) {
4450
path, err := os.MkdirTemp(os.TempDir(), "bundle")
4551
Expect(err).ToNot(HaveOccurred())
4652
defer os.RemoveAll(path)
@@ -56,6 +62,24 @@ var _ = Describe("Bundle Loader", func() {
5662
f(file.Name())
5763
}
5864

65+
withTempRegistry := func(f func(endpoint string)) {
66+
logLogger := log.Logger{}
67+
logLogger.SetOutput(GinkgoWriter)
68+
69+
s := httptest.NewServer(
70+
registry.New(
71+
registry.Logger(&logLogger),
72+
registry.WithReferrersSupport(true),
73+
),
74+
)
75+
defer s.Close()
76+
77+
u, err := url.Parse(s.URL)
78+
Expect(err).ToNot(HaveOccurred())
79+
80+
f(u.Host)
81+
}
82+
5983
filecontent := func(path string) string {
6084
data, err := os.ReadFile(path)
6185
Expect(err).ToNot(HaveOccurred())
@@ -188,14 +212,16 @@ var _ = Describe("Bundle Loader", func() {
188212
})
189213

190214
AfterEach(func() {
191-
ref, err := name.ParseReference(testImage)
192-
Expect(err).ToNot(HaveOccurred())
215+
if testImage != "" {
216+
ref, err := name.ParseReference(testImage)
217+
Expect(err).ToNot(HaveOccurred())
193218

194-
options, auth, err := image.GetOptions(context.TODO(), ref, true, dockerConfigFile, "test-agent")
195-
Expect(err).ToNot(HaveOccurred())
219+
options, auth, err := image.GetOptions(context.TODO(), ref, true, dockerConfigFile, "test-agent")
220+
Expect(err).ToNot(HaveOccurred())
196221

197-
// Delete test image (best effort)
198-
_ = image.Delete(ref, options, *auth)
222+
// Delete test image (best effort)
223+
_ = image.Delete(ref, options, *auth)
224+
}
199225
})
200226

201227
It("should pull and unpack an image from a private registry", func() {
@@ -232,4 +258,62 @@ var _ = Describe("Bundle Loader", func() {
232258
})
233259
})
234260
})
261+
262+
Context("Result file checks", func() {
263+
tmpFile := func(dir string, name string, data []byte, timestamp time.Time) {
264+
var path = filepath.Join(dir, name)
265+
266+
Expect(os.WriteFile(
267+
path,
268+
data,
269+
os.FileMode(0644),
270+
)).To(Succeed())
271+
272+
Expect(os.Chtimes(
273+
path,
274+
timestamp,
275+
timestamp,
276+
)).To(Succeed())
277+
}
278+
279+
// Creates a controlled reference image with one file called "file" with modification
280+
// timestamp of Friday, February 13, 2009 11:31:30 PM (unix timestamp 1234567890)
281+
withReferenceImage := func(f func(dig name.Digest)) {
282+
withTempRegistry(func(endpoint string) {
283+
withTempDir(func(target string) {
284+
timestamp := time.Unix(1234567890, 0)
285+
286+
ref, err := name.ParseReference(fmt.Sprintf("%s/namespace/image:tag", endpoint))
287+
Expect(err).ToNot(HaveOccurred())
288+
Expect(ref).ToNot(BeNil())
289+
290+
tmpFile(target, "file", []byte("foobar"), timestamp)
291+
292+
dig, err := bundle.PackAndPush(ref, target)
293+
Expect(err).ToNot(HaveOccurred())
294+
Expect(dig).ToNot(BeNil())
295+
296+
f(dig)
297+
})
298+
})
299+
}
300+
301+
It("should store source timestamp in result file", func() {
302+
withTempDir(func(target string) {
303+
withTempDir(func(result string) {
304+
withReferenceImage(func(dig name.Digest) {
305+
resultSourceTimestamp := filepath.Join(result, "source-timestamp")
306+
307+
Expect(run(
308+
"--image", dig.String(),
309+
"--target", target,
310+
"--result-file-source-timestamp", resultSourceTimestamp,
311+
)).To(Succeed())
312+
313+
Expect(filecontent(resultSourceTimestamp)).To(Equal("1234567890"))
314+
})
315+
})
316+
})
317+
})
318+
})
235319
})

cmd/git/main.go

+27-14
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,21 @@ func (e ExitError) Error() string {
4646
}
4747

4848
type settings struct {
49-
help bool
50-
url string
51-
revision string
52-
depth uint
53-
target string
54-
resultFileCommitSha string
55-
resultFileCommitAuthor string
56-
resultFileBranchName string
57-
secretPath string
58-
skipValidation bool
59-
gitURLRewrite bool
60-
resultFileErrorMessage string
61-
resultFileErrorReason string
62-
verbose bool
49+
help bool
50+
url string
51+
revision string
52+
depth uint
53+
target string
54+
resultFileCommitSha string
55+
resultFileCommitAuthor string
56+
resultFileBranchName string
57+
resultFileSourceTimestamp string
58+
secretPath string
59+
skipValidation bool
60+
gitURLRewrite bool
61+
resultFileErrorMessage string
62+
resultFileErrorReason string
63+
verbose bool
6364
}
6465

6566
var flagValues settings
@@ -81,6 +82,7 @@ func init() {
8182
pflag.StringVar(&flagValues.target, "target", "", "The target directory of the clone operation")
8283
pflag.StringVar(&flagValues.resultFileCommitSha, "result-file-commit-sha", "", "A file to write the commit sha to.")
8384
pflag.StringVar(&flagValues.resultFileCommitAuthor, "result-file-commit-author", "", "A file to write the commit author to.")
85+
pflag.StringVar(&flagValues.resultFileSourceTimestamp, "result-file-source-timestamp", "", "A file to write the source timestamp to.")
8486
pflag.StringVar(&flagValues.resultFileBranchName, "result-file-branch-name", "", "A file to write the branch name to.")
8587
pflag.StringVar(&flagValues.secretPath, "secret-path", "", "A directory that contains a secret. Either username and password for basic authentication. Or a SSH private key and optionally a known hosts file. Optional.")
8688

@@ -180,6 +182,17 @@ func runGitClone(ctx context.Context) error {
180182
}
181183
}
182184

185+
if flagValues.resultFileSourceTimestamp != "" {
186+
output, err := git(ctx, "-C", flagValues.target, "show", "--no-patch", "--format=%ct")
187+
if err != nil {
188+
return err
189+
}
190+
191+
if err = os.WriteFile(flagValues.resultFileSourceTimestamp, []byte(output), 0644); err != nil {
192+
return err
193+
}
194+
}
195+
183196
if strings.TrimSpace(flagValues.revision) == "" && strings.TrimSpace(flagValues.resultFileBranchName) != "" {
184197
output, err := git(ctx, "-C", flagValues.target, "rev-parse", "--abbrev-ref", "HEAD")
185198
if err != nil {

cmd/git/main_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,21 @@ var _ = Describe("Git Resource", func() {
471471
})
472472
})
473473
})
474+
475+
It("should store source-timestamp into file specified in --result-file-source-timestamp flag", func() {
476+
withTempFile("source-timestamp", func(filename string) {
477+
withTempDir(func(target string) {
478+
Expect(run(withArgs(
479+
"--url", exampleRepo,
480+
"--target", target,
481+
"--revision", "v0.1.0",
482+
"--result-file-source-timestamp", filename,
483+
))).ToNot(HaveOccurred())
484+
485+
Expect(filecontent(filename)).To(Equal("1619426578"))
486+
})
487+
})
488+
})
474489
})
475490

476491
Context("Some tests mutate or depend on git configurations. They must run sequentially to avoid race-conditions.", Ordered, func() {

deploy/crds/shipwright.io_buildruns.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -6334,6 +6334,13 @@ spec:
63346334
name:
63356335
description: Name is the name of source
63366336
type: string
6337+
timestamp:
6338+
description: Timestamp holds the timestamp of the source, which
6339+
depends on the actual source type and could range from being
6340+
the commit timestamp or the fileystem timestamp of the most
6341+
recent source file in the working directory
6342+
format: date-time
6343+
type: string
63376344
required:
63386345
- name
63396346
type: object
@@ -12552,6 +12559,13 @@ spec:
1255212559
description: Digest hold the image digest result
1255312560
type: string
1255412561
type: object
12562+
timestamp:
12563+
description: Timestamp holds the timestamp of the source, which
12564+
depends on the actual source type and could range from being
12565+
the commit timestamp or the fileystem timestamp of the most
12566+
recent source file in the working directory
12567+
format: date-time
12568+
type: string
1255512569
type: object
1255612570
startTime:
1255712571
description: StartTime is the time the build is actually started.

pkg/apis/build/v1alpha1/buildrun_types.go

+8
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ type SourceResult struct {
117117
//
118118
// +optional
119119
Bundle *BundleSourceResult `json:"bundle,omitempty"`
120+
121+
// Timestamp holds the timestamp of the source, which
122+
// depends on the actual source type and could range from
123+
// being the commit timestamp or the fileystem timestamp
124+
// of the most recent source file in the working directory
125+
//
126+
// +optional
127+
Timestamp *metav1.Time `json:"timestamp,omitempty"`
120128
}
121129

122130
// BundleSourceResult holds the results emitted from the bundle source

0 commit comments

Comments
 (0)