blob: f2cb788e0389b19b4734217ce087bdca122f2755 [file]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"testing"
"time"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"infra/cmd/cloudbuildhelper/cloudbuild"
"infra/cmd/cloudbuildhelper/fileset"
"infra/cmd/cloudbuildhelper/manifest"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
const (
testTargetName = "test-name"
testBucketName = "test-bucket"
testRegistryName = "fake.example.com/registry"
testDigest = "sha256:totally-legit-hash"
testTagName = "canonical-tag"
testLogURL = "https://example.com/cloud-build-log"
testImageName = testRegistryName + "/" + testTargetName
)
var _true = true // for *bool
var (
notifyCfg1 = manifest.NotifyConfig{"a": 1}
notifyCfg2 = manifest.NotifyConfig{"b": 1}
)
func TestBuild(t *testing.T) {
t.Parallel()
Convey("With mocks", t, func() {
testTime := time.Date(2016, time.February, 3, 4, 5, 6, 0, time.Local)
ctx, tc := testclock.UseTime(context.Background(), testTime)
tc.SetTimerCallback(func(d time.Duration, t clock.Timer) {
if testclock.HasTags(t, "sleep-timer") {
tc.Add(d)
}
})
ctx, _ = clock.WithTimeout(ctx, 20*time.Minute) // don't hang forever
store := newStorageImplMock()
registry := newRegistryImplMock()
builder := newBuilderImplMock(registry)
fs, digest := prepFileSet()
var (
// Path relative to the storage root.
testTarballPath = fmt.Sprintf("%s/%s.tar.gz", testTargetName, digest)
// Where we drops the tarball, excluding "#<generation>" suffix.
testTarballURL = fmt.Sprintf("gs://%s/%s/%s.tar.gz", testBucketName, testTargetName, digest)
)
builder.outputDigests = func(img string) string {
So(img, ShouldEqual, testImageName+":cbh")
return testDigest
}
Convey("Never seen before tarball", func() {
builder.provenance = func(gs string) string {
So(gs, ShouldEqual, testTarballURL+"#1") // used first gen
return digest // got its digest correctly
}
res, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{
Name: testTargetName,
Deterministic: &_true,
},
Image: testImageName,
BuildID: "b1",
CanonicalTag: testTagName,
Tags: []string{"latest"},
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
Notify: []manifest.NotifyConfig{notifyCfg1, notifyCfg2},
})
So(err, ShouldBeNil)
// Uploaded the file.
obj, err := store.Check(ctx, testTarballPath)
So(err, ShouldBeNil)
So(obj.String(), ShouldEqual, testTarballURL+"#1") // uploaded the first gen
// Used Cloud Build.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: testDigest,
CanonicalTag: testTagName,
BuildID: "b1",
},
ViewBuildURL: testLogURL,
Notify: []manifest.NotifyConfig{notifyCfg1, notifyCfg2},
})
// Tagged it with canonical tag.
img, err := registry.GetImage(ctx, fmt.Sprintf("%s:%s", testImageName, testTagName))
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, testDigest)
// And moved "latest" tag.
img, err = registry.GetImage(ctx, testImageName+":latest")
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, testDigest)
// Now we build this exact tarball again using different canonical tag.
// We should get back the image we've already built.
Convey("Building existing tarball deterministically: reuses the image", func() {
builder.provenance = func(gs string) string {
panic("Cloud Build should not be invoked")
}
// To avoid clashing on metadata keys that depend on timestamps.
tc.Add(time.Minute)
res, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{
Name: testTargetName,
Deterministic: &_true,
},
Image: testImageName,
BuildID: "b2",
CanonicalTag: "another-tag",
Tags: []string{"pushed"},
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
})
So(err, ShouldBeNil)
// Reused the existing image.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: testDigest,
CanonicalTag: testTagName,
BuildID: "b1", // was build there
Timestamp: testTime.Add(10 * time.Second),
},
})
// And moved "pushed" tag, even through no new image was built.
img, err := registry.GetImage(ctx, testImageName+":pushed")
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, testDigest)
// Both builds are associated with the tarball via its metadata now.
tarball, err := store.Check(ctx, testTarballPath)
So(err, ShouldBeNil)
md := tarball.Metadata.Values(buildRefMetaKey)
So(md, ShouldHaveLength, 2)
So(md[0].Value, ShouldEqual, `{"build_id":"b2","tag":"another-tag"}`)
So(md[1].Value, ShouldEqual, `{"build_id":"b1","tag":"canonical-tag"}`)
})
// Now we build this exact tarball again using different canonical tag,
// but mark the target as non-deterministic. It should ignore the existing
// image and build a new one.
Convey("Building existing tarball non-deterministically: creates new image", func() {
builder.provenance = func(gs string) string {
So(gs, ShouldEqual, testTarballURL+"#1") // reused first gen
return digest
}
builder.outputDigests = func(img string) string {
So(img, ShouldEqual, testImageName+":cbh")
return "sha256:new-totally-legit-hash"
}
// To avoid clashing on metadata keys that depend on timestamps.
tc.Add(time.Minute)
res, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{
Name: testTargetName,
Deterministic: nil,
},
Image: testImageName,
BuildID: "b2",
CanonicalTag: "another-tag",
Tags: []string{"latest"},
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
})
So(err, ShouldBeNil)
// Built the new image.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: "sha256:new-totally-legit-hash",
CanonicalTag: "another-tag",
BuildID: "b2",
},
ViewBuildURL: testLogURL,
})
// And moved "latest" tag.
img, err = registry.GetImage(ctx, testImageName+":latest")
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, "sha256:new-totally-legit-hash")
// Both builds are associated with the tarball via its metadata now.
tarball, err := store.Check(ctx, testTarballPath)
So(err, ShouldBeNil)
md := tarball.Metadata.Values(buildRefMetaKey)
So(md, ShouldHaveLength, 2)
So(md[0].Value, ShouldEqual, `{"build_id":"b2","tag":"another-tag"}`)
So(md[1].Value, ShouldEqual, `{"build_id":"b1","tag":"canonical-tag"}`)
})
})
Convey("Already seen canonical tag", func() {
registry.put(fmt.Sprintf("%s:%s", testImageName, testTagName), testDigest)
res, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{Name: testTargetName},
Image: testImageName,
CanonicalTag: testTagName,
Tags: []string{"latest"},
Registry: registry,
})
So(err, ShouldBeNil)
// Reused the existing image.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: testDigest,
CanonicalTag: testTagName,
},
})
// And moved "latest" tag.
img, err := registry.GetImage(ctx, testImageName+":latest")
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, testDigest)
})
Convey("Using :inputs-hash as canonical tag", func() {
expectedTag := "cbh-inputs-" + digest[:24]
builder.provenance = func(gs string) string {
So(gs, ShouldEqual, testTarballURL+"#1") // used first gen
return digest // got its digest correctly
}
params := buildParams{
Manifest: &manifest.Manifest{
Name: testTargetName,
Deterministic: &_true,
},
Image: testImageName,
BuildID: "b1",
CanonicalTag: inputsHashCanonicalTag,
Tags: []string{"latest"},
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
}
res, err := runBuild(ctx, params)
So(err, ShouldBeNil)
// Uploaded the file.
obj, err := store.Check(ctx, testTarballPath)
So(err, ShouldBeNil)
So(obj.String(), ShouldEqual, testTarballURL+"#1") // uploaded the first gen
// Used Cloud Build.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: testDigest,
CanonicalTag: expectedTag,
BuildID: "b1",
},
ViewBuildURL: testLogURL,
})
// Tagged it with canonical tag.
img, err := registry.GetImage(ctx, fmt.Sprintf("%s:%s", testImageName, expectedTag))
So(err, ShouldBeNil)
So(img.Digest, ShouldEqual, testDigest)
// Repeating the build reuses the existing image since inputs hash didn't
// change (and thus its canonical tag also didn't change and we already
// have an image with this canonical tag).
res, err = runBuild(ctx, params)
So(err, ShouldBeNil)
So(res, ShouldResemble, buildResult{
Name: testTargetName,
Image: &imageRef{
Image: testImageName,
Digest: testDigest,
CanonicalTag: expectedTag,
},
ViewBuildURL: "", // Cloud Build wasn't used
})
})
Convey("No registry is set => nothing is uploaded", func() {
builder.provenance = func(gs string) string {
So(gs, ShouldEqual, testTarballURL+"#1") // used first gen
return digest // got its digest correctly
}
res, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{Name: testTargetName},
CanonicalTag: testTagName, // ignored
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
})
So(err, ShouldBeNil)
// Uploaded the file.
obj, err := store.Check(ctx, testTarballPath)
So(err, ShouldBeNil)
So(obj.String(), ShouldEqual, testTarballURL+"#1") // uploaded the first gen
// Did NOT produce any image, but have a link to the build.
So(res, ShouldResemble, buildResult{
Name: testTargetName,
ViewBuildURL: testLogURL,
})
})
Convey("Cloud Build build failure", func() {
builder.finalStatus = cloudbuild.StatusFailure
_, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{Name: testTargetName},
Image: testImageName,
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
})
So(err, ShouldErrLike, "build failed, see its logs")
})
Convey("Cloud Build API errors", func() {
builder.checkCallback = func(b *runningBuild) error {
return fmt.Errorf("boom")
}
_, err := runBuild(ctx, buildParams{
Manifest: &manifest.Manifest{Name: testTargetName},
Image: testImageName,
Stage: stageFileSet(fs),
Store: store,
Builder: builder,
Registry: registry,
})
So(err, ShouldErrLike, "waiting for the build to finish: too many errors, the last one: boom")
})
})
}
////////////////////////////////////////////////////////////////////////////////
func prepFileSet() (fs *fileset.Set, digest string) {
fs = &fileset.Set{}
fs.AddFromMemory("Dockerfile", []byte("boo-boo-boo"), nil)
fs.AddFromMemory("dir/something", []byte("ba-ba-ba"), nil)
h := sha256.New()
if err := fs.ToTarGz(h); err != nil {
panic(err)
}
digest = hex.EncodeToString(h.Sum(nil))
return
}
func stageFileSet(fs *fileset.Set) stageCallback {
return func(c context.Context, m *manifest.Manifest, cb func(*fileset.Set) error) error {
return cb(fs)
}
}