mirror of
https://github.com/ko-build/ko.git
synced 2025-11-26 22:40:38 +02:00
1331 lines
35 KiB
Go
1331 lines
35 KiB
Go
// Copyright 2018 ko Build Authors All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package build
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
|
"github.com/google/go-containerregistry/pkg/v1/random"
|
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
|
"github.com/google/ko/pkg/internal/gittesting"
|
|
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/sigstore/cosign/v2/pkg/oci"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func repoRootDir() (string, error) {
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
return "", fmt.Errorf("could not get current filename")
|
|
}
|
|
basepath := filepath.Dir(filename)
|
|
repoDir := filepath.Join(basepath, "..", "..")
|
|
return filepath.Rel(basepath, repoDir)
|
|
}
|
|
|
|
func TestGoBuildQualifyImport(t *testing.T) {
|
|
base, err := random.Image(1024, 1)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
|
|
repoDir, err := repoRootDir()
|
|
if err != nil {
|
|
t.Fatalf("could not get Git repository root directory")
|
|
}
|
|
|
|
tests := []struct {
|
|
description string
|
|
rawImportpath string
|
|
dir string
|
|
qualifiedImportpath string
|
|
expectError bool
|
|
}{
|
|
{
|
|
description: "strict qualified import path",
|
|
rawImportpath: "ko://github.com/google/ko",
|
|
dir: "",
|
|
qualifiedImportpath: "ko://github.com/google/ko",
|
|
expectError: false,
|
|
},
|
|
{
|
|
description: "strict qualified import path in subdirectory of go.mod",
|
|
rawImportpath: "ko://github.com/google/ko/test",
|
|
dir: "",
|
|
qualifiedImportpath: "ko://github.com/google/ko/test",
|
|
expectError: false,
|
|
},
|
|
{
|
|
description: "non-strict qualified import path",
|
|
rawImportpath: "github.com/google/ko",
|
|
dir: "",
|
|
qualifiedImportpath: "ko://github.com/google/ko",
|
|
expectError: false,
|
|
},
|
|
{
|
|
description: "non-strict local import path in repository root directory",
|
|
rawImportpath: "./test",
|
|
dir: repoDir,
|
|
qualifiedImportpath: "ko://github.com/google/ko/test",
|
|
expectError: false,
|
|
},
|
|
{
|
|
description: "non-strict local import path in subdirectory",
|
|
rawImportpath: ".",
|
|
dir: filepath.Join(repoDir, "test"),
|
|
qualifiedImportpath: "ko://github.com/google/ko/test",
|
|
expectError: false,
|
|
},
|
|
{
|
|
description: "non-existent non-strict local import path",
|
|
rawImportpath: "./does-not-exist",
|
|
dir: "/",
|
|
qualifiedImportpath: "should return error",
|
|
expectError: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
ng, err := NewGo(context.Background(), test.dir, WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil }))
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
gotImportpath, err := ng.QualifyImport(test.rawImportpath)
|
|
if err != nil && test.expectError {
|
|
return
|
|
}
|
|
if err != nil && !test.expectError {
|
|
t.Errorf("QualifyImport(dir=%q)(%q) was error (%v), want nil error", test.dir, test.rawImportpath, err)
|
|
}
|
|
if err == nil && test.expectError {
|
|
t.Errorf("QualifyImport(dir=%q)(%q) was nil error, want non-nil error", test.dir, test.rawImportpath)
|
|
}
|
|
if gotImportpath != test.qualifiedImportpath {
|
|
t.Errorf("QualifyImport(dir=%q)(%q) = (%q, nil), want (%q, nil)", test.dir, test.rawImportpath, gotImportpath, test.qualifiedImportpath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var baseRef = name.MustParseReference("all.your/base")
|
|
|
|
func TestGoBuildIsSupportedRef(t *testing.T) {
|
|
base, err := random.Image(1024, 3)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
|
|
ng, err := NewGo(context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil }))
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
// Supported import paths.
|
|
for _, importpath := range []string{
|
|
"ko://github.com/google/ko", // ko can build itself.
|
|
} {
|
|
t.Run(importpath, func(t *testing.T) {
|
|
if err := ng.IsSupportedReference(importpath); err != nil {
|
|
t.Errorf("IsSupportedReference(%q) = (%v), want nil", importpath, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Unsupported import paths.
|
|
for _, importpath := range []string{
|
|
"ko://github.com/google/ko/pkg/build", // not a command.
|
|
"ko://github.com/google/ko/pkg/nonexistent", // does not exist.
|
|
} {
|
|
t.Run(importpath, func(t *testing.T) {
|
|
if err := ng.IsSupportedReference(importpath); err == nil {
|
|
t.Errorf("IsSupportedReference(%v) = nil, want error", importpath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoBuildIsSupportedRefWithModules(t *testing.T) {
|
|
base, err := random.Image(1024, 3)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
|
|
opts := []Option{
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
}
|
|
|
|
ng, err := NewGo(context.Background(), "", opts...)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
// Supported import paths.
|
|
for _, importpath := range []string{
|
|
"ko://github.com/google/ko/test", // ko can build the test package.
|
|
"ko://github.com/go-training/helloworld", // ko can build commands in dependent modules
|
|
} {
|
|
t.Run(importpath, func(t *testing.T) {
|
|
if err := ng.IsSupportedReference(importpath); err != nil {
|
|
t.Errorf("IsSupportedReference(%q) = (%v), want nil", err, importpath)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Unsupported import paths.
|
|
for _, importpath := range []string{
|
|
"ko://github.com/google/ko/pkg/build", // not a command.
|
|
"ko://github.com/google/ko/pkg/nonexistent", // does not exist.
|
|
"ko://github.com/google/go-github", // not in this module.
|
|
} {
|
|
t.Run(importpath, func(t *testing.T) {
|
|
if err := ng.IsSupportedReference(importpath); err == nil {
|
|
t.Errorf("IsSupportedReference(%v) = nil, want error", importpath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildEnv(t *testing.T) {
|
|
tests := []struct {
|
|
description string
|
|
platform v1.Platform
|
|
osEnv []string
|
|
globalEnv []string
|
|
configEnv []string
|
|
expectedEnvs map[string]string
|
|
}{{
|
|
description: "defaults",
|
|
platform: v1.Platform{
|
|
OS: "linux",
|
|
Architecture: "amd64",
|
|
},
|
|
expectedEnvs: map[string]string{
|
|
"GOOS": "linux",
|
|
"GOARCH": "amd64",
|
|
"CGO_ENABLED": "0",
|
|
},
|
|
}, {
|
|
description: "override a default value",
|
|
osEnv: []string{"CGO_ENABLED=0"},
|
|
configEnv: []string{"CGO_ENABLED=1"},
|
|
expectedEnvs: map[string]string{
|
|
"CGO_ENABLED": "1",
|
|
},
|
|
}, {
|
|
description: "global override a default value",
|
|
osEnv: []string{"CGO_ENABLED=0"},
|
|
globalEnv: []string{"CGO_ENABLED=1"},
|
|
expectedEnvs: map[string]string{
|
|
"CGO_ENABLED": "1",
|
|
},
|
|
}, {
|
|
description: "override a global value",
|
|
globalEnv: []string{"CGO_ENABLED=0"},
|
|
configEnv: []string{"CGO_ENABLED=1"},
|
|
expectedEnvs: map[string]string{
|
|
"CGO_ENABLED": "1",
|
|
},
|
|
}, {
|
|
description: "override an envvar and add an envvar",
|
|
osEnv: []string{"CGO_ENABLED=0"},
|
|
configEnv: []string{"CGO_ENABLED=1", "GOPRIVATE=git.internal.example.com,source.developers.google.com"},
|
|
expectedEnvs: map[string]string{
|
|
"CGO_ENABLED": "1",
|
|
"GOPRIVATE": "git.internal.example.com,source.developers.google.com",
|
|
},
|
|
}, {
|
|
description: "arm variant",
|
|
platform: v1.Platform{
|
|
Architecture: "arm",
|
|
Variant: "v7",
|
|
},
|
|
expectedEnvs: map[string]string{
|
|
"GOARCH": "arm",
|
|
"GOARM": "7",
|
|
},
|
|
}, {
|
|
// GOARM is ignored for arm64.
|
|
description: "arm64 variant",
|
|
platform: v1.Platform{
|
|
Architecture: "arm64",
|
|
Variant: "v8",
|
|
},
|
|
expectedEnvs: map[string]string{
|
|
"GOARCH": "arm64",
|
|
"GOARM": "",
|
|
},
|
|
}, {
|
|
description: "amd64 variant",
|
|
platform: v1.Platform{
|
|
Architecture: "amd64",
|
|
Variant: "v3",
|
|
},
|
|
expectedEnvs: map[string]string{
|
|
"GOARCH": "amd64",
|
|
"GOAMD64": "v3",
|
|
},
|
|
}}
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
env, err := buildEnv(test.platform, test.osEnv, test.globalEnv, test.configEnv)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error running buildEnv(): %v", err)
|
|
}
|
|
envs := map[string]string{}
|
|
for _, e := range env {
|
|
split := strings.SplitN(e, "=", 2)
|
|
envs[split[0]] = split[1]
|
|
}
|
|
for key, val := range test.expectedEnvs {
|
|
if envs[key] != val {
|
|
t.Errorf("buildEnv(): expected %s=%s, got %s=%s", key, val, key, envs[key])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateTemplateData(t *testing.T) {
|
|
t.Run("env", func(t *testing.T) {
|
|
t.Setenv("FOO", "bar")
|
|
params := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
|
|
vars := params["Env"].(map[string]string)
|
|
require.Equal(t, "bar", vars["FOO"])
|
|
})
|
|
|
|
t.Run("empty creation time", func(t *testing.T) {
|
|
params := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
|
|
|
|
// Make sure the date was set to time.Now().
|
|
actualDateStr := params["Date"].(string)
|
|
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
|
|
require.NoError(t, err)
|
|
if time.Since(actualDate) > time.Minute {
|
|
t.Fatalf("expected date to be now, but was %v", actualDate)
|
|
}
|
|
|
|
// Check the timestamp.
|
|
actualTimestampSec := params["Timestamp"].(int64)
|
|
actualTimestamp := time.Unix(actualTimestampSec, 0).UTC()
|
|
expectedTimestamp := actualDate.Truncate(time.Second).UTC()
|
|
require.Equal(t, expectedTimestamp, actualTimestamp)
|
|
})
|
|
|
|
t.Run("creation time", func(t *testing.T) {
|
|
// Create a reference time for use as a creation time.
|
|
expectedTime, err := time.Parse(time.RFC3339, "2012-11-01T22:08:00Z")
|
|
require.NoError(t, err)
|
|
|
|
params := createTemplateData(context.TODO(), buildContext{
|
|
creationTime: v1.Time{Time: expectedTime},
|
|
dir: t.TempDir(),
|
|
})
|
|
|
|
// Check the date.
|
|
actualDateStr := params["Date"].(string)
|
|
actualDate, err := time.Parse(time.RFC3339, actualDateStr)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedTime, actualDate)
|
|
|
|
// Check the timestamp.
|
|
actualTimestampSec := params["Timestamp"].(int64)
|
|
actualTimestamp := time.Unix(actualTimestampSec, 0).UTC()
|
|
require.Equal(t, expectedTime, actualTimestamp)
|
|
})
|
|
|
|
t.Run("no git available", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
params := createTemplateData(context.TODO(), buildContext{dir: dir})
|
|
gitParams := params["Git"].(map[string]interface{})
|
|
|
|
require.Equal(t, "", gitParams["Branch"])
|
|
require.Equal(t, "", gitParams["Tag"])
|
|
require.Equal(t, "", gitParams["ShortCommit"])
|
|
require.Equal(t, "", gitParams["FullCommit"])
|
|
require.Equal(t, "clean", gitParams["TreeState"])
|
|
})
|
|
|
|
t.Run("git", func(t *testing.T) {
|
|
// Create a fake git structure under the test temp dir.
|
|
const fakeGitURL = "git@github.com:foo/bar.git"
|
|
dir := t.TempDir()
|
|
gittesting.GitInit(t, dir)
|
|
gittesting.GitRemoteAdd(t, dir, fakeGitURL)
|
|
gittesting.GitCommit(t, dir, "commit1")
|
|
gittesting.GitTag(t, dir, "v0.0.1")
|
|
|
|
params := createTemplateData(context.TODO(), buildContext{dir: dir})
|
|
gitParams := params["Git"].(map[string]interface{})
|
|
|
|
require.Equal(t, "main", gitParams["Branch"])
|
|
require.Equal(t, "v0.0.1", gitParams["Tag"])
|
|
require.Equal(t, "clean", gitParams["TreeState"])
|
|
})
|
|
}
|
|
|
|
func TestBuildConfig(t *testing.T) {
|
|
tests := []struct {
|
|
description string
|
|
options []Option
|
|
importpath string
|
|
expectConfig Config
|
|
}{
|
|
{
|
|
description: "minimal options",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
},
|
|
},
|
|
{
|
|
description: "trimpath flag",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
WithTrimpath(true),
|
|
},
|
|
expectConfig: Config{
|
|
Flags: FlagArray{"-trimpath"},
|
|
},
|
|
},
|
|
{
|
|
description: "no trimpath flag",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
WithTrimpath(false),
|
|
},
|
|
},
|
|
{
|
|
description: "build config and trimpath",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
WithConfig(map[string]Config{
|
|
"example.com/foo": {
|
|
Flags: FlagArray{"-v"},
|
|
},
|
|
}),
|
|
WithTrimpath(true),
|
|
},
|
|
importpath: "example.com/foo",
|
|
expectConfig: Config{
|
|
Flags: FlagArray{"-v", "-trimpath"},
|
|
},
|
|
},
|
|
{
|
|
description: "no trimpath overridden by build config flag",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
WithConfig(map[string]Config{
|
|
"example.com/bar": {
|
|
Flags: FlagArray{"-trimpath"},
|
|
},
|
|
}),
|
|
WithTrimpath(false),
|
|
},
|
|
importpath: "example.com/bar",
|
|
expectConfig: Config{
|
|
Flags: FlagArray{"-trimpath"},
|
|
},
|
|
},
|
|
{
|
|
description: "disable optimizations",
|
|
options: []Option{
|
|
WithBaseImages(nilGetBase),
|
|
WithDisabledOptimizations(),
|
|
},
|
|
expectConfig: Config{
|
|
Flags: FlagArray{"-gcflags", "all=-N -l"},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
i, err := NewGo(context.Background(), "", test.options...)
|
|
if err != nil {
|
|
t.Fatalf("NewGo(): unexpected error: %+v", err)
|
|
}
|
|
gb, ok := i.(*gobuild)
|
|
if !ok {
|
|
t.Fatal("NewGo() did not return *gobuild{} as expected")
|
|
}
|
|
config := gb.configForImportPath(test.importpath)
|
|
if diff := cmp.Diff(test.expectConfig, config, cmpopts.EquateEmpty(),
|
|
cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" {
|
|
t.Errorf("%T differ (-got, +want): %s", test.expectConfig, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func nilGetBase(context.Context, string) (name.Reference, Result, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
const wantSBOM = "This is our fake SBOM"
|
|
|
|
// A helper method we use to substitute for the default "build" method.
|
|
func fauxSBOM(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error) {
|
|
return []byte(wantSBOM), "application/vnd.garbage", nil
|
|
}
|
|
|
|
// A helper method we use to substitute for the default "build" method.
|
|
func writeTempFile(_ context.Context, buildCtx buildContext) (string, error) {
|
|
tmpDir, err := os.MkdirTemp("", "ko")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
file, err := os.CreateTemp(tmpDir, "out")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
if _, err := file.WriteString(filepath.ToSlash(buildCtx.ip)); err != nil {
|
|
return "", err
|
|
}
|
|
return file.Name(), nil
|
|
}
|
|
|
|
func TestGoBuildNoKoData(t *testing.T) {
|
|
baseLayers := int64(3)
|
|
base, err := random.Image(1024, baseLayers)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
WithPlatforms("all"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
result, err := ng.Build(context.Background(), StrictScheme+importpath)
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
img, ok := result.(v1.Image)
|
|
if !ok {
|
|
t.Fatalf("Build() not an Image: %T", result)
|
|
}
|
|
|
|
ls, err := img.Layers()
|
|
if err != nil {
|
|
t.Fatalf("Layers() = %v", err)
|
|
}
|
|
|
|
// Check that we have the expected number of layers.
|
|
t.Run("check layer count", func(t *testing.T) {
|
|
// We get a layer for the go binary and a layer for the kodata/
|
|
if got, want := int64(len(ls)), baseLayers+2; got != want {
|
|
t.Fatalf("len(Layers()) = %v, want %v", got, want)
|
|
}
|
|
})
|
|
|
|
// Check that rebuilding the image again results in the same image digest.
|
|
t.Run("check determinism", func(t *testing.T) {
|
|
result2, err := ng.Build(context.Background(), StrictScheme+importpath)
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
d1, err := result.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
d2, err := result2.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
|
|
if d1 != d2 {
|
|
t.Errorf("Digest mismatch: %s != %s", d1, d2)
|
|
}
|
|
})
|
|
|
|
// Check that the entrypoint of the image is configured to invoke our Go application
|
|
t.Run("check entrypoint", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
entrypoint := cfg.Config.Entrypoint
|
|
if got, want := len(entrypoint), 1; got != want {
|
|
t.Errorf("len(entrypoint) = %v, want %v", got, want)
|
|
}
|
|
|
|
if got, want := entrypoint[0], "/ko-app/ko"; got != want {
|
|
t.Errorf("entrypoint = %v, want %v", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("check creation time", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
|
|
actual := cfg.Created
|
|
if actual.Time != creationTime.Time {
|
|
t.Errorf("created = %v, want %v", actual, creationTime)
|
|
}
|
|
})
|
|
}
|
|
|
|
func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) {
|
|
t.Helper()
|
|
|
|
ls, err := img.Layers()
|
|
if err != nil {
|
|
t.Fatalf("Layers() = %v", err)
|
|
}
|
|
|
|
// Check that we have the expected number of layers.
|
|
t.Run("check layer count", func(t *testing.T) {
|
|
// We get a layer for the go binary and a layer for the kodata/
|
|
if got, want := int64(len(ls)), baseLayers+2; got != want {
|
|
t.Fatalf("len(Layers()) = %v, want %v", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("check app layer contents", func(t *testing.T) {
|
|
dataLayer := ls[baseLayers]
|
|
|
|
if _, err := dataLayer.Digest(); err != nil {
|
|
t.Errorf("Digest() = %v", err)
|
|
}
|
|
// We don't check the data layer here because it includes a symlink of refs and
|
|
// will produce a distinct hash each time we commit something.
|
|
|
|
r, err := dataLayer.Uncompressed()
|
|
if err != nil {
|
|
t.Errorf("Uncompressed() = %v", err)
|
|
}
|
|
defer r.Close()
|
|
tr := tar.NewReader(r)
|
|
if _, err := tr.Next(); errors.Is(err, io.EOF) {
|
|
t.Errorf("Layer contained no files")
|
|
}
|
|
})
|
|
|
|
// Check that the kodata layer contains the expected data (even though it was a symlink
|
|
// outside kodata).
|
|
t.Run("check kodata", func(t *testing.T) {
|
|
dataLayer := ls[baseLayers]
|
|
r, err := dataLayer.Uncompressed()
|
|
if err != nil {
|
|
t.Errorf("Uncompressed() = %v", err)
|
|
}
|
|
defer r.Close()
|
|
found := false
|
|
tr := tar.NewReader(r)
|
|
for {
|
|
header, err := tr.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
} else if err != nil {
|
|
t.Errorf("Next() = %v", err)
|
|
continue
|
|
}
|
|
if header.Name != path.Join(kodataRoot, "kenobi") {
|
|
continue
|
|
}
|
|
found = true
|
|
body, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
t.Errorf("ReadAll() = %v", err)
|
|
} else if want, got := "Hello there\n", string(body); got != want {
|
|
t.Errorf("ReadAll() = %v, wanted %v", got, want)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Didn't find expected file in tarball")
|
|
}
|
|
})
|
|
|
|
// Check that the entrypoint of the image is configured to invoke our Go application
|
|
t.Run("check entrypoint", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
entrypoint := cfg.Config.Entrypoint
|
|
if got, want := len(entrypoint), 1; got != want {
|
|
t.Errorf("len(entrypoint) = %v, want %v", got, want)
|
|
}
|
|
|
|
if got, want := entrypoint[0], "/ko-app/test"; got != want {
|
|
t.Errorf("entrypoint = %v, want %v", got, want)
|
|
}
|
|
})
|
|
|
|
// Check that the environment contains the KO_DATA_PATH environment variable.
|
|
t.Run("check KO_DATA_PATH env var", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
found := false
|
|
for _, entry := range cfg.Config.Env {
|
|
if entry == "KO_DATA_PATH="+kodataRoot {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Didn't find KO_DATA_PATH.")
|
|
}
|
|
})
|
|
|
|
// Check that PATH contains the directory of the produced binary.
|
|
t.Run("check PATH env var", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
found := false
|
|
for _, envVar := range cfg.Config.Env {
|
|
if strings.HasPrefix(envVar, "PATH=") {
|
|
pathValue := strings.TrimPrefix(envVar, "PATH=")
|
|
pathEntries := strings.Split(pathValue, ":")
|
|
for _, pathEntry := range pathEntries {
|
|
if pathEntry == "/ko-app" {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Didn't find entrypoint in PATH.")
|
|
}
|
|
})
|
|
|
|
t.Run("check creation time", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Errorf("ConfigFile() = %v", err)
|
|
}
|
|
|
|
actual := cfg.Created
|
|
if actual.Time != creationTime.Time {
|
|
t.Errorf("created = %v, want %v", actual, creationTime)
|
|
}
|
|
})
|
|
|
|
t.Run("check annotations", func(t *testing.T) {
|
|
if !checkAnnotations {
|
|
t.Skip("skipping annotations check")
|
|
}
|
|
mf, err := img.Manifest()
|
|
if err != nil {
|
|
t.Fatalf("Manifest() = %v", err)
|
|
}
|
|
t.Logf("Got annotations: %v", mf.Annotations)
|
|
if _, found := mf.Annotations[specsv1.AnnotationBaseImageDigest]; !found {
|
|
t.Errorf("image annotations did not contain base image digest")
|
|
}
|
|
want := baseRef.Name()
|
|
if got := mf.Annotations[specsv1.AnnotationBaseImageName]; got != want {
|
|
t.Errorf("base image ref; got %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
if expectSBOM {
|
|
t.Run("checking for SBOM", func(t *testing.T) {
|
|
f, err := img.Attachment("sbom")
|
|
if err != nil {
|
|
t.Fatalf("Attachment() = %v", err)
|
|
}
|
|
b, err := f.Payload()
|
|
if err != nil {
|
|
t.Fatalf("Payload() = %v", err)
|
|
}
|
|
t.Logf("Got SBOM: %v", string(b))
|
|
if string(b) != wantSBOM {
|
|
t.Errorf("got SBOM %s, wanted %s", string(b), wantSBOM)
|
|
}
|
|
})
|
|
} else {
|
|
t.Run("checking for no SBOM", func(t *testing.T) {
|
|
f, err := img.Attachment("sbom")
|
|
if err == nil {
|
|
b, err := f.Payload()
|
|
if err != nil {
|
|
t.Fatalf("Payload() = %v", err)
|
|
}
|
|
t.Fatalf("Attachment() = %v, wanted error", string(b))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoBuild(t *testing.T) {
|
|
baseLayers := int64(3)
|
|
base, err := random.Image(1024, baseLayers)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
WithLabel("foo", "bar"),
|
|
WithLabel("hello", "world"),
|
|
WithPlatforms("all"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
img, ok := result.(oci.SignedImage)
|
|
if !ok {
|
|
t.Fatalf("Build() not a SignedImage: %T", result)
|
|
}
|
|
|
|
validateImage(t, img, baseLayers, creationTime, true, true)
|
|
|
|
// Check that rebuilding the image again results in the same image digest.
|
|
t.Run("check determinism", func(t *testing.T) {
|
|
result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
d1, err := result.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
d2, err := result2.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
|
|
if d1 != d2 {
|
|
t.Errorf("Digest mismatch: %s != %s", d1, d2)
|
|
}
|
|
})
|
|
|
|
t.Run("check labels", func(t *testing.T) {
|
|
cfg, err := img.ConfigFile()
|
|
if err != nil {
|
|
t.Fatalf("ConfigFile() = %v", err)
|
|
}
|
|
|
|
want := map[string]string{
|
|
"foo": "bar",
|
|
"hello": "world",
|
|
}
|
|
got := cfg.Config.Labels
|
|
if d := cmp.Diff(got, want); d != "" {
|
|
t.Fatalf("Labels diff (-got,+want): %s", d)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGoBuildWithKOCACHE(t *testing.T) {
|
|
now := time.Now() // current local time
|
|
sec := now.Unix()
|
|
tmpDir := t.TempDir()
|
|
koCacheDir := filepath.Join(tmpDir, strconv.FormatInt(sec, 10))
|
|
|
|
t.Setenv("KOCACHE", koCacheDir)
|
|
baseLayers := int64(3)
|
|
base, err := random.Image(1024, baseLayers)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
WithPlatforms("all"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
_, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
t.Run("check KOCACHE exists", func(t *testing.T) {
|
|
_, err := os.Stat(koCacheDir)
|
|
if os.IsNotExist(err) {
|
|
t.Fatalf("KOCACHE directory %s should be exists= %v", koCacheDir, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGoBuildWithoutSBOM(t *testing.T) {
|
|
baseLayers := int64(3)
|
|
base, err := random.Image(1024, baseLayers)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
WithLabel("foo", "bar"),
|
|
WithLabel("hello", "world"),
|
|
WithDisabledSBOM(),
|
|
WithPlatforms("all"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
img, ok := result.(oci.SignedImage)
|
|
if !ok {
|
|
t.Fatalf("Build() not a SignedImage: %T", result)
|
|
}
|
|
|
|
validateImage(t, img, baseLayers, creationTime, true, false)
|
|
}
|
|
|
|
func TestGoBuildIndex(t *testing.T) {
|
|
baseLayers := int64(3)
|
|
images := int64(2)
|
|
base, err := random.Index(1024, baseLayers, images)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
WithPlatforms("all"),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
idx, ok := result.(oci.SignedImageIndex)
|
|
if !ok {
|
|
t.Fatalf("Build() not a SignedImageIndex: %T", result)
|
|
}
|
|
|
|
im, err := idx.IndexManifest()
|
|
if err != nil {
|
|
t.Fatalf("IndexManifest() = %v", err)
|
|
}
|
|
|
|
for _, desc := range im.Manifests {
|
|
img, err := idx.SignedImage(desc.Digest)
|
|
if err != nil {
|
|
t.Fatalf("idx.Image(%s) = %v", desc.Digest, err)
|
|
}
|
|
validateImage(t, img, baseLayers, creationTime, false, true)
|
|
}
|
|
|
|
if want, got := images, int64(len(im.Manifests)); want != got {
|
|
t.Fatalf("len(Manifests()) = %v, want %v", got, want)
|
|
}
|
|
|
|
// Check that rebuilding the image again results in the same image digest.
|
|
t.Run("check determinism", func(t *testing.T) {
|
|
result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
d1, err := result.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
d2, err := result2.Digest()
|
|
if err != nil {
|
|
t.Fatalf("Digest() = %v", err)
|
|
}
|
|
|
|
if d1 != d2 {
|
|
t.Errorf("Digest mismatch: %s != %s", d1, d2)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNestedIndex(t *testing.T) {
|
|
baseLayers := int64(3)
|
|
images := int64(2)
|
|
base, err := random.Index(1024, baseLayers, images)
|
|
if err != nil {
|
|
t.Fatalf("random.Image() = %v", err)
|
|
}
|
|
importpath := "github.com/google/ko"
|
|
|
|
nestedBase := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: base})
|
|
|
|
creationTime := v1.Time{Time: time.Unix(5000, 0)}
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithCreationTime(creationTime),
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, nestedBase, nil }),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
_, err = ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
|
|
if err == nil {
|
|
t.Fatal("Build() expected err")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "unexpected mediaType") {
|
|
t.Errorf("Build() expected unexpected mediaType error, got: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestGoarm(t *testing.T) {
|
|
// From golang@sha256:1ba0da74b20aad52b091877b0e0ece503c563f39e37aa6b0e46777c4d820a2ae
|
|
// and made up invalid cases.
|
|
for _, tc := range []struct {
|
|
platform v1.Platform
|
|
variant string
|
|
err bool
|
|
}{{
|
|
platform: v1.Platform{
|
|
Architecture: "arm",
|
|
OS: "linux",
|
|
Variant: "vnot-a-number",
|
|
},
|
|
err: true,
|
|
}, {
|
|
platform: v1.Platform{
|
|
Architecture: "arm",
|
|
OS: "linux",
|
|
Variant: "wrong-prefix",
|
|
},
|
|
err: true,
|
|
}, {
|
|
platform: v1.Platform{
|
|
Architecture: "arm64",
|
|
OS: "linux",
|
|
Variant: "v3",
|
|
},
|
|
variant: "",
|
|
}, {
|
|
platform: v1.Platform{
|
|
Architecture: "arm",
|
|
OS: "linux",
|
|
Variant: "v5",
|
|
},
|
|
variant: "5",
|
|
}, {
|
|
platform: v1.Platform{
|
|
Architecture: "arm",
|
|
OS: "linux",
|
|
Variant: "v7",
|
|
},
|
|
variant: "7",
|
|
}, {
|
|
platform: v1.Platform{
|
|
Architecture: "arm64",
|
|
OS: "linux",
|
|
Variant: "v8",
|
|
},
|
|
variant: "7",
|
|
},
|
|
} {
|
|
variant, err := getGoarm(tc.platform)
|
|
if tc.err {
|
|
if err == nil {
|
|
t.Errorf("getGoarm(%v) expected err", tc.platform)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("getGoarm failed for %v: %v", tc.platform, err)
|
|
}
|
|
if got, want := variant, tc.variant; got != want {
|
|
t.Errorf("wrong variant for %v: want %q got %q", tc.platform, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatchesPlatformSpec(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
platform *v1.Platform
|
|
spec []string
|
|
result bool
|
|
err bool
|
|
}{{
|
|
platform: nil,
|
|
spec: []string{"all"},
|
|
result: true,
|
|
}, {
|
|
platform: nil,
|
|
spec: []string{"linux/amd64"},
|
|
result: false,
|
|
}, {
|
|
platform: &v1.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
spec: []string{"all"},
|
|
result: true,
|
|
}, {
|
|
platform: &v1.Platform{
|
|
Architecture: "amd64",
|
|
OS: "windows",
|
|
},
|
|
spec: []string{"linux"},
|
|
result: false,
|
|
}, {
|
|
platform: &v1.Platform{
|
|
Architecture: "arm64",
|
|
OS: "linux",
|
|
Variant: "v3",
|
|
},
|
|
spec: []string{"linux/amd64", "linux/arm64"},
|
|
result: true,
|
|
}, {
|
|
platform: &v1.Platform{
|
|
Architecture: "arm64",
|
|
OS: "linux",
|
|
Variant: "v3",
|
|
},
|
|
spec: []string{"linux/amd64", "linux/arm64/v4"},
|
|
result: false,
|
|
}, {
|
|
platform: &v1.Platform{
|
|
Architecture: "arm64",
|
|
OS: "linux",
|
|
Variant: "v3",
|
|
},
|
|
spec: []string{"linux/amd64", "linux/arm64/v3/z5"},
|
|
err: true,
|
|
}, {
|
|
spec: []string{},
|
|
platform: &v1.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
result: false,
|
|
}, {
|
|
// Exact match w/ osversion
|
|
spec: []string{"windows/amd64:10.0.17763.1234"},
|
|
platform: &v1.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763.1234",
|
|
},
|
|
result: true,
|
|
}, {
|
|
// OSVersion partial match using relaxed semantics.
|
|
spec: []string{"windows/amd64:10.0.17763"},
|
|
platform: &v1.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763.1234",
|
|
},
|
|
result: true,
|
|
}, {
|
|
// Not windows and osversion isn't exact match.
|
|
spec: []string{"linux/amd64:10.0.17763"},
|
|
platform: &v1.Platform{
|
|
OS: "linux",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763.1234",
|
|
},
|
|
result: false,
|
|
}, {
|
|
// Not matching X.Y.Z
|
|
spec: []string{"windows/amd64:10"},
|
|
platform: &v1.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763.1234",
|
|
},
|
|
result: false,
|
|
}, {
|
|
// Requirement is more specific.
|
|
spec: []string{"windows/amd64:10.0.17763.1234"},
|
|
platform: &v1.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763", // this won't happen in the wild, but it shouldn't match.
|
|
},
|
|
result: false,
|
|
}, {
|
|
// Requirement is not specific enough.
|
|
spec: []string{"windows/amd64:10.0.17763.1234"},
|
|
platform: &v1.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
OSVersion: "10.0.17763.1234.5678", // this won't happen in the wild, but it shouldn't match.
|
|
},
|
|
result: false,
|
|
}, {
|
|
// Even --platform=all does not match unknown/unknown.
|
|
platform: &v1.Platform{Architecture: "unknown", OS: "unknown"},
|
|
spec: []string{"all"},
|
|
result: false,
|
|
}} {
|
|
pm, err := parseSpec(tc.spec)
|
|
if tc.err {
|
|
if err == nil {
|
|
t.Errorf("parseSpec(%v, %q) expected err", tc.platform, tc.spec)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parseSpec failed for %v %q: %v", tc.platform, tc.spec, err)
|
|
}
|
|
matches := pm.matches(tc.platform)
|
|
if got, want := matches, tc.result; got != want {
|
|
t.Errorf("wrong result for %v %q: want %t got %t", tc.platform, tc.spec, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoBuildConsistentMediaTypes(t *testing.T) {
|
|
for _, c := range []struct {
|
|
desc string
|
|
mediaType, layerMediaType types.MediaType
|
|
}{{
|
|
desc: "docker types",
|
|
mediaType: types.DockerManifestSchema2,
|
|
layerMediaType: types.DockerLayer,
|
|
}, {
|
|
desc: "oci types",
|
|
mediaType: types.OCIManifestSchema1,
|
|
layerMediaType: types.OCILayer,
|
|
}} {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
base := mutate.MediaType(empty.Image, c.mediaType)
|
|
l, err := random.Layer(10, c.layerMediaType)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base, err = mutate.AppendLayers(base, l)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ng, err := NewGo(
|
|
context.Background(),
|
|
"",
|
|
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
|
|
withBuilder(writeTempFile),
|
|
withSBOMber(fauxSBOM),
|
|
WithPlatforms("all"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewGo() = %v", err)
|
|
}
|
|
|
|
importpath := "github.com/google/ko"
|
|
|
|
result, err := ng.Build(context.Background(), StrictScheme+importpath)
|
|
if err != nil {
|
|
t.Fatalf("Build() = %v", err)
|
|
}
|
|
|
|
img, ok := result.(v1.Image)
|
|
if !ok {
|
|
t.Fatalf("Build() not an Image: %T", result)
|
|
}
|
|
|
|
ls, err := img.Layers()
|
|
if err != nil {
|
|
t.Fatalf("Layers() = %v", err)
|
|
}
|
|
|
|
for i, l := range ls {
|
|
gotMT, err := l.MediaType()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotMT != c.layerMediaType {
|
|
t.Errorf("layer %d: got mediaType %q, want %q", i, gotMT, c.layerMediaType)
|
|
}
|
|
}
|
|
|
|
gotMT, err := img.MediaType()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotMT != c.mediaType {
|
|
t.Errorf("got image mediaType %q, want %q", gotMT, c.layerMediaType)
|
|
}
|
|
})
|
|
}
|
|
}
|