1
0
mirror of https://github.com/ko-build/ko.git synced 2025-07-12 23:50:31 +02:00

Enable embedding of ko publish (#348)

- Export functions and a variable to enable embedding of ko's
  `publish` functionality to be embedded in other tools.

  See https://github.com/GoogleContainerTools/skaffold/pull/5611

- Remove DockerRepo PublishOption and flag.

  This removes the `DockerRepo` config option and `--docker-repo`
  flag from the PR.

  New PR with the extracted config option:
  https://github.com/google/ko/pull/351

- Fix copyright headers for boilerplate check.

- Use DockerRepo PublishOption instead of env var.

- Override defaultBaseImage using BuildOptions.

  Remove exported package global SetDefaultBaseImage and instead
  allow programmatic override of the default base image using
  the field `BaseImage` in `options.BuildOptions`.

  Also fix copyright header years.

- Add BuildOptions parameter to getBaseImage

  This enables access to BaseImage for programmatically overriding
  the default base image from `.ko.yaml`.

- Add UserAgent to BuildOptions and PublishOptions

  This enables programmatically overriding the `User-Agent` HTTP
  request header for both pulling the base image and pushing the
  built image.

- Rename MakeBuilder to NewBuilder and MakePublisher to NewPublisher.

  For more idiomatic constructor function names.
This commit is contained in:
Halvard Skogsrud
2021-05-26 04:44:52 +10:00
committed by GitHub
parent 21728fdbb0
commit d6b3a3cba3
8 changed files with 292 additions and 46 deletions

View File

@ -1,16 +1,18 @@
// Copyright 2018 Google LLC All Rights Reserved. /*
// Copyright 2018 Google LLC 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. Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at 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
// 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, Unless required by applicable law or agreed to in writing, software
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// limitations under the License. See the License for the specific language governing permissions and
limitations under the License.
*/
package commands package commands
@ -31,6 +33,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/ko/pkg/build" "github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands/options"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -39,7 +42,9 @@ var (
baseImageOverrides map[string]name.Reference baseImageOverrides map[string]name.Reference
) )
func getBaseImage(platform string) build.GetBase { // getBaseImage returns a function that determines the base image for a given import path.
// If the `bo.BaseImage` parameter is non-empty, it overrides base image configuration from `.ko.yaml`.
func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase {
return func(ctx context.Context, s string) (build.Result, error) { return func(ctx context.Context, s string) (build.Result, error) {
s = strings.TrimPrefix(s, build.StrictScheme) s = strings.TrimPrefix(s, build.StrictScheme)
// Viper configuration file keys are case insensitive, and are // Viper configuration file keys are case insensitive, and are
@ -52,9 +57,20 @@ func getBaseImage(platform string) build.GetBase {
if !ok { if !ok {
ref = defaultBaseImage ref = defaultBaseImage
} }
if bo.BaseImage != "" {
var err error
ref, err = name.ParseReference(bo.BaseImage)
if err != nil {
return nil, fmt.Errorf("parsing bo.BaseImage (%q): %v", bo.BaseImage, err)
}
}
userAgent := ua()
if bo.UserAgent != "" {
userAgent = bo.UserAgent
}
ropt := []remote.Option{ ropt := []remote.Option{
remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithAuthFromKeychain(authn.DefaultKeychain),
remote.WithUserAgent(ua()), remote.WithUserAgent(userAgent),
remote.WithContext(ctx), remote.WithContext(ctx),
} }

View File

@ -0,0 +1,47 @@
/*
Copyright 2021 Google LLC 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 commands
import (
"context"
"testing"
"github.com/google/ko/pkg/commands/options"
)
func TestOverrideDefaultBaseImageUsingBuildOption(t *testing.T) {
wantDigest := "sha256:76c39a6f76890f8f8b026f89e081084bc8c64167d74e6c93da7a053cb4ccb5dd"
wantImage := "gcr.io/distroless/static-debian9@" + wantDigest
bo := &options.BuildOptions{
BaseImage: wantImage,
}
baseFn := getBaseImage("all", bo)
res, err := baseFn(context.Background(), "ko://example.com/helloworld")
if err != nil {
t.Fatalf("getBaseImage(): %v", err)
}
digest, err := res.Digest()
if err != nil {
t.Fatalf("res.Digest(): %v", err)
}
gotDigest := digest.String()
if gotDigest != wantDigest {
t.Errorf("got digest %s, wanted %s", gotDigest, wantDigest)
}
}

View File

@ -22,10 +22,16 @@ import (
// BuildOptions represents options for the ko builder. // BuildOptions represents options for the ko builder.
type BuildOptions struct { type BuildOptions struct {
// BaseImage enables setting the default base image programmatically.
// If non-empty, this takes precedence over the value in `.ko.yaml`.
BaseImage string
ConcurrentBuilds int ConcurrentBuilds int
DisableOptimizations bool DisableOptimizations bool
Platform string Platform string
Labels []string Labels []string
// UserAgent enables overriding the default value of the `User-Agent` HTTP
// request header used when retrieving the base image.
UserAgent string
} }
func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) {

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2021 Google LLC All Rights Reserved. Copyright 2018 Google LLC All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -35,6 +35,10 @@ type PublishOptions struct {
// LocalDomain overrides the default domain for images loaded into the local Docker daemon. Use with Local=true. // LocalDomain overrides the default domain for images loaded into the local Docker daemon. Use with Local=true.
LocalDomain string LocalDomain string
// UserAgent enables overriding the default value of the `User-Agent` HTTP
// request header used when pushing the built image to an image registry.
UserAgent string
Tags []string Tags []string
// TagOnly resolves images into tag-only references. // TagOnly resolves images into tag-only references.
TagOnly bool TagOnly bool

View File

@ -1,16 +1,18 @@
// Copyright 2018 Google LLC All Rights Reserved. /*
// Copyright 2018 Google LLC 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. Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at 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
// 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, Unless required by applicable law or agreed to in writing, software
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// limitations under the License. See the License for the specific language governing permissions and
limitations under the License.
*/
package commands package commands
@ -41,6 +43,11 @@ func qualifyLocalImport(importpath string) (string, error) {
return pkgs[0].PkgPath, nil return pkgs[0].PkgPath, nil
} }
// PublishImages publishes images
func PublishImages(ctx context.Context, importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) {
return publishImages(ctx, importpaths, pub, b)
}
func publishImages(ctx context.Context, importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) { func publishImages(ctx context.Context, importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) {
imgs := make(map[string]name.Reference) imgs := make(map[string]name.Reference)
for _, importpath := range importpaths { for _, importpath := range importpaths {

View File

@ -0,0 +1,100 @@
/*
Copyright 2021 Google LLC 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 commands
import (
"context"
"fmt"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands/options"
)
func TestPublishImages(t *testing.T) {
repo := "registry.example.com/repository"
sampleAppDir, err := sampleAppRelDir()
if err != nil {
t.Fatalf("sampleAppRelDir(): %v", err)
}
tests := []struct {
description string
publishArg string
importpath string
}{
{
description: "import path with ko scheme",
publishArg: "ko://github.com/google/ko/test",
importpath: "github.com/google/ko/test",
},
{
description: "import path without ko scheme",
publishArg: "github.com/google/ko/test",
importpath: "github.com/google/ko/test",
},
{
description: "file path",
publishArg: sampleAppDir,
importpath: "github.com/google/ko/test",
},
}
for _, test := range tests {
ctx := context.Background()
bo := &options.BuildOptions{
ConcurrentBuilds: 1,
}
builder, err := NewBuilder(ctx, bo)
if err != nil {
t.Fatalf("%s: MakeBuilder(): %v", test.description, err)
}
po := &options.PublishOptions{
DockerRepo: repo,
PreserveImportPaths: true,
}
publisher, err := NewPublisher(po)
if err != nil {
t.Fatalf("%s: MakePublisher(): %v", test.description, err)
}
importpathWithScheme := build.StrictScheme + test.importpath
refs, err := PublishImages(ctx, []string{test.publishArg}, publisher, builder)
if err != nil {
t.Fatalf("%s: PublishImages(): %v", test.description, err)
}
ref, exists := refs[importpathWithScheme]
if !exists {
t.Errorf("%s: could not find image for importpath %s", test.description, importpathWithScheme)
}
gotImageName := ref.Context().Name()
wantImageName := strings.ToLower(fmt.Sprintf("%s/%s", repo, test.importpath))
if gotImageName != wantImageName {
t.Errorf("%s: got %s, wanted %s", test.description, gotImageName, wantImageName)
}
}
}
func sampleAppRelDir() (string, error) {
_, filename, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("could not get current filename")
}
basepath := filepath.Dir(filename)
testAppDir := filepath.Join(basepath, "..", "..", "test")
return filepath.Rel(basepath, testAppDir)
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2021 Google LLC All Rights Reserved. Copyright 2018 Google LLC All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -41,6 +41,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
) )
// ua returns the ko user agent.
func ua() string { func ua() string {
if v := version(); v != "" { if v := version(); v != "" {
return "ko/" + v return "ko/" + v
@ -79,7 +80,7 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
} }
opts := []build.Option{ opts := []build.Option{
build.WithBaseImages(getBaseImage(platform)), build.WithBaseImages(getBaseImage(platform, bo)),
build.WithPlatforms(platform), build.WithPlatforms(platform),
} }
if creationTime != nil { if creationTime != nil {
@ -98,6 +99,11 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
return opts, nil return opts, nil
} }
// NewBuilder creates a ko builder
func NewBuilder(ctx context.Context, bo *options.BuildOptions) (build.Interface, error) {
return makeBuilder(ctx, bo)
}
func makeBuilder(ctx context.Context, bo *options.BuildOptions) (*build.Caching, error) { func makeBuilder(ctx context.Context, bo *options.BuildOptions) (*build.Caching, error) {
opt, err := gobuildOptions(bo) opt, err := gobuildOptions(bo)
if err != nil { if err != nil {
@ -129,6 +135,11 @@ func makeBuilder(ctx context.Context, bo *options.BuildOptions) (*build.Caching,
return build.NewCaching(innerBuilder) return build.NewCaching(innerBuilder)
} }
// NewPublisher creates a ko publisher
func NewPublisher(po *options.PublishOptions) (publish.Interface, error) {
return makePublisher(po)
}
func makePublisher(po *options.PublishOptions) (publish.Interface, error) { func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
// Create the publish.Interface that we will use to publish image references // Create the publish.Interface that we will use to publish image references
// to either a docker daemon or a container image registry. // to either a docker daemon or a container image registry.
@ -151,7 +162,7 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
} }
if _, err := name.NewRegistry(repoName); err != nil { if _, err := name.NewRegistry(repoName); err != nil {
if _, err := name.NewRepository(repoName); err != nil { if _, err := name.NewRepository(repoName); err != nil {
return nil, fmt.Errorf("failed to parse environment variable KO_DOCKER_REPO=%q as repository: %v", repoName, err) return nil, fmt.Errorf("failed to parse %q as repository: %v", repoName, err)
} }
} }
@ -167,9 +178,13 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
tp := publish.NewTarball(po.TarballFile, repoName, namer, po.Tags) tp := publish.NewTarball(po.TarballFile, repoName, namer, po.Tags)
publishers = append(publishers, tp) publishers = append(publishers, tp)
} }
userAgent := ua()
if po.UserAgent != "" {
userAgent = po.UserAgent
}
if po.Push { if po.Push {
dp, err := publish.NewDefault(repoName, dp, err := publish.NewDefault(repoName,
publish.WithUserAgent(ua()), publish.WithUserAgent(userAgent),
publish.WithAuthFromKeychain(authn.DefaultKeychain), publish.WithAuthFromKeychain(authn.DefaultKeychain),
publish.WithNamer(namer), publish.WithNamer(namer),
publish.WithTags(po.Tags), publish.WithTags(po.Tags),
@ -208,6 +223,7 @@ type nopPublisher struct {
} }
func (n nopPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) { func (n nopPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
h, err := br.Digest() h, err := br.Digest()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,16 +1,18 @@
// Copyright 2018 Google LLC All Rights Reserved. /*
// Copyright 2018 Google LLC 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. Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at 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
// 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, Unless required by applicable law or agreed to in writing, software
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// limitations under the License. See the License for the specific language governing permissions and
limitations under the License.
*/
package commands package commands
@ -20,12 +22,14 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/ko/pkg/build" "github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands/options" "github.com/google/ko/pkg/commands/options"
@ -134,6 +138,52 @@ kind: Bar
} }
} }
func TestMakeBuilder(t *testing.T) {
ctx := context.Background()
bo := &options.BuildOptions{
ConcurrentBuilds: 1,
}
builder, err := NewBuilder(ctx, bo)
if err != nil {
t.Fatalf("MakeBuilder(): %v", err)
}
res, err := builder.Build(ctx, "ko://github.com/google/ko/test")
if err != nil {
t.Fatalf("builder.Build(): %v", err)
}
gotDigest, err := res.Digest()
if err != nil {
t.Fatalf("res.Digest(): %v", err)
}
fmt.Println(gotDigest.String())
}
func TestMakePublisher(t *testing.T) {
repo := "registry.example.com/repository"
po := &options.PublishOptions{
DockerRepo: repo,
PreserveImportPaths: true,
}
publisher, err := NewPublisher(po)
if err != nil {
t.Fatalf("MakePublisher(): %v", err)
}
defer publisher.Close()
ctx := context.Background()
importpath := "github.com/google/ko/test"
importpathWithScheme := build.StrictScheme + importpath
buildResult := empty.Index
ref, err := publisher.Publish(ctx, buildResult, importpathWithScheme)
if err != nil {
t.Fatalf("publisher.Publish(): %v", err)
}
gotImageName := ref.Context().Name()
wantImageName := strings.ToLower(fmt.Sprintf("%s/%s", repo, importpath))
if gotImageName != wantImageName {
t.Errorf("got %s, wanted %s", gotImageName, wantImageName)
}
}
func mustRepository(s string) name.Repository { func mustRepository(s string) name.Repository {
n, err := name.NewRepository(s) n, err := name.NewRepository(s)
if err != nil { if err != nil {