Skip to content

Commit

Permalink
Add core ko builder implementation
Browse files Browse the repository at this point in the history
This adds the `Build()` method for building artifacts using ko. It
supports both publishing the resulting image to a registry, and
sideloading it to the local Docker daemon.

The `temporary.go` file contains the structs intended to be added to the
schema.

This implementation is still missing the following features:

- integration test
- dependencies (for file watching)
- insecure registries
- debug mode
- support for `go` flags and environment variables (waiting on
  ko-build/ko#340)
- actually plumbing the builder into the Skaffold CLI and API :-)

Tracking: GoogleContainerTools#6041
  • Loading branch information
halvards committed Jun 22, 2021
1 parent 30fded0 commit e3cd36b
Show file tree
Hide file tree
Showing 10 changed files with 613 additions and 9 deletions.
24 changes: 24 additions & 0 deletions go.sum

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions pkg/skaffold/build/ko/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright 2021 The Skaffold Authors
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 ko

import (
"context"
"fmt"
"io"
"strings"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/publish"

// latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1"
latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko/schema"
)

// Build an artifact using ko
func (b *Builder) Build(ctx context.Context, out io.Writer, a *latestV1.Artifact, ref string) (string, error) {
koBuilder, err := b.newKoBuilder(ctx, a)
if err != nil {
return "", fmt.Errorf("error creating ko builder: %w", err)
}

koPublisher, err := b.newKoPublisher(ref)
if err != nil {
return "", fmt.Errorf("error creating ko publisher: %w", err)
}
defer koPublisher.Close()

imageRef, err := b.buildAndPublish(ctx, a.ImageName, koBuilder, koPublisher)
if err != nil {
return "", fmt.Errorf("could not build and publish ko image %q: %w", a.ImageName, err)
}
fmt.Fprintln(out, imageRef.Name())

return b.getImageIdentifier(ctx, imageRef, ref)
}

// buildAndPublish the image using the ko builder and publisher.
func (b *Builder) buildAndPublish(ctx context.Context, imageName string, koBuilder build.Interface, koPublisher publish.Interface) (name.Reference, error) {
importpath, err := getImportPath(imageName, koBuilder)
if err != nil {
return nil, fmt.Errorf("could not determine Go import path for ko image %q: %w", imageName, err)
}
imageMap, err := b.publishImages(ctx, []string{importpath}, koPublisher, koBuilder)
if err != nil {
return nil, fmt.Errorf("failed to publish ko image: %w", err)
}
imageRef, exists := imageMap[importpath]
if !exists {
return nil, fmt.Errorf("no built image found for Go import path %q build images: %+v", importpath, imageMap)
}
return imageRef, nil
}

// getImportPath determines the Go import path that ko should build.
//
// If the image name from the Skaffold config has the prefix `ko://`, then
// treat the remainder of the string as the Go import path to build. This
// matches current ko behavior for working with Kubernetes resource files, and
// it will allow ko users to easily migrate to Skaffold without changing their
// Kubernetes YAML files. See https://github.com/google/ko#yaml-changes.
//
// If the image name does _not_ start with `ko://`, determine the Go import
// path of the image workspace directory.
func getImportPath(imageName string, koBuilder build.Interface) (string, error) {
if strings.HasPrefix(imageName, build.StrictScheme) {
return imageName, nil
}
return koBuilder.QualifyImport(".")
}

// getImageIdentifier returns the image tag or digest for published images (`pushImages=true`),
// or the image ID from the local Docker daemon for sideloaded images (`pushImages=false`).
func (b *Builder) getImageIdentifier(ctx context.Context, imageRef name.Reference, ref string) (string, error) {
if b.pushImages {
return imageRef.Identifier(), nil
}
imageIdentifier, err := b.localDocker.ImageID(ctx, ref)
if err != nil {
return "", fmt.Errorf("could not get imageID from local Docker Daemon for image %s: %+v", ref, err)
}
return imageIdentifier, nil
}
129 changes: 129 additions & 0 deletions pkg/skaffold/build/ko/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright 2021 The Skaffold Authors
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 ko

import (
"bytes"
"context"
"strings"
"testing"

"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/publish"

// latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1"
latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko/schema"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/testutil"
)

// koImportPath is the import path of this package, with the ko scheme prefix.
const koImportPath = "ko://github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko"

func TestBuildKoImages(t *testing.T) {
tests := []struct {
description string
ref string
imageID string
pushImages bool
importpath string
imageNameFromConfig string
workspace string
}{
{
description: "simple image name in config and sideload image",
ref: "gcr.io/project-id/test-app1:testTag",
imageID: "imageID",
pushImages: false,
importpath: koImportPath,
imageNameFromConfig: "test-app1",
},
{
description: "ko import path used in image name config and sideload image",
ref: "gcr.io/project-id/example.com/myapp:myTag",
imageID: "imageID",
pushImages: false,
importpath: "ko://example.com/myapp",
imageNameFromConfig: "ko://example.com/myapp",
},
{
description: "simple image name in config and push image",
ref: "gcr.io/project-id/test-app2:testTag",
imageID: "testTag",
pushImages: true,
importpath: "ko://github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko",
imageNameFromConfig: "test-app2",
},
{
description: "ko import path used in image name config and push image",
ref: "gcr.io/project-id/example.com/myapp:myTag",
imageID: "myTag",
pushImages: true,
importpath: "ko://example.com/myapp",
imageNameFromConfig: "ko://example.com/myapp",
},
}
for _, test := range tests {
testutil.Run(t, test.description, func(t *testutil.T) {
b := stubKoArtifactBuilder(test.ref, test.imageID, test.pushImages, test.importpath)

artifact := &latestV1.Artifact{
ImageName: test.imageNameFromConfig,
ArtifactType: latestV1.ArtifactType{
KoArtifact: &latestV1.KoArtifact{},
},
Workspace: test.workspace,
Dependencies: []*latestV1.ArtifactDependency{},
}

var outBuffer bytes.Buffer
gotImageID, err := b.Build(context.TODO(), &outBuffer, artifact, test.ref)
t.CheckNoError(err)
if gotImageID != test.imageID {
t.Errorf("got image ID %s, wanted %s", gotImageID, test.imageID)
}
imageNameOut := strings.TrimSuffix(outBuffer.String(), "\n")
if imageNameOut != test.ref {
t.Errorf("image name output was %q, wanted %q", imageNameOut, test.ref)
}
})
}
}

func stubKoArtifactBuilder(ref string, imageID string, pushImages bool, importpath string) *Builder {
api := (&testutil.FakeAPIClient{}).Add(ref, imageID)
localDocker := fakeLocalDockerDaemon(api)
b := NewArtifactBuilder(localDocker, pushImages)

// Fake implementation of the `publishImages` function.
b.publishImages = func(_ context.Context, _ []string, _ publish.Interface, _ build.Interface) (map[string]name.Reference, error) {
imageRef, err := name.ParseReference(ref)
if err != nil {
return nil, err
}
return map[string]name.Reference{
importpath: imageRef,
}, nil
}
return b
}

func fakeLocalDockerDaemon(api client.CommonAPIClient) docker.LocalDaemon {
return docker.NewLocalDaemon(api, nil, false, nil)
}
45 changes: 45 additions & 0 deletions pkg/skaffold/build/ko/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2021 The Skaffold Authors
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 ko

import (
"context"
"strings"

"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands"
"github.com/google/ko/pkg/commands/options"

// latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1"
latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/ko/schema"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/version"
)

func (b *Builder) newKoBuilder(ctx context.Context, a *latestV1.Artifact) (build.Interface, error) {
bo := buildOptions(a.KoArtifact.BaseImage, a.KoArtifact.Platforms, a.Workspace)
return commands.NewBuilder(ctx, bo)
}

func buildOptions(baseImage string, platforms []string, workspace string) *options.BuildOptions {
return &options.BuildOptions{
BaseImage: baseImage,
ConcurrentBuilds: 1,
Platform: strings.Join(platforms, ","),
UserAgent: version.UserAgentWithClient(),
WorkingDirectory: workspace,
}
}
75 changes: 75 additions & 0 deletions pkg/skaffold/build/ko/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2021 The Skaffold Authors
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 ko

import (
"testing"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/version"
"github.com/GoogleContainerTools/skaffold/testutil"
)

func TestBuildOptions(t *testing.T) {
tests := []struct {
description string
baseImage string
platforms []string
wantPlatform string
workspace string
}{
{
description: "all zero value",
},
{
description: "empty platforms",
platforms: []string{},
},
{
description: "base image",
baseImage: "gcr.io/distroless/static:nonroot",
},
{
description: "multiple platforms",
platforms: []string{"linux/amd64", "linux/arm64"},
wantPlatform: "linux/amd64,linux/arm64",
},
{
description: "workspace",
workspace: "my-app-subdirectory",
},
}
for _, test := range tests {
testutil.Run(t, test.description, func(t *testutil.T) {
bo := buildOptions(test.baseImage, test.platforms, test.workspace)
if bo.BaseImage != test.baseImage {
t.Errorf("wanted BaseImage (%q), got (%q)", test.baseImage, bo.BaseImage)
}
if bo.ConcurrentBuilds < 1 {
t.Errorf("ConcurrentBuilds must always be >= 1 for the ko builder")
}
if bo.Platform != test.wantPlatform {
t.Errorf("wanted platform (%q), got (%q)", test.wantPlatform, bo.Platform)
}
if bo.UserAgent != version.UserAgentWithClient() {
t.Errorf("need user agent for fetching the base image")
}
if bo.WorkingDirectory != test.workspace {
t.Errorf("wanted WorkingDirectory (%q), got (%q)", test.workspace, bo.WorkingDirectory)
}
})
}
}
29 changes: 25 additions & 4 deletions pkg/skaffold/build/ko/ko.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,30 @@ limitations under the License.
package ko

import (
kobuild "github.com/google/ko/pkg/build"
"context"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/commands"
"github.com/google/ko/pkg/publish"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
)

// KoScheme is the prefix used to disambiguate image references and Go import paths.
// Adding the const here to force import of a ko package.
const KoScheme = kobuild.StrictScheme
// Builder is an artifact builder that uses ko
type Builder struct {
localDocker docker.LocalDaemon
pushImages bool

// publishImages can be overridden for unit testing purposes.
publishImages func(context.Context, []string, publish.Interface, build.Interface) (map[string]name.Reference, error)
}

// NewArtifactBuilder returns a new ko artifact builder
func NewArtifactBuilder(localDocker docker.LocalDaemon, pushImages bool) *Builder {
return &Builder{
localDocker: localDocker,
pushImages: pushImages,
publishImages: commands.PublishImages,
}
}
Loading

0 comments on commit e3cd36b

Please sign in to comment.