Skip to content

Commit 412ab14

Browse files
committed
feat: kfn build uses ko as default
1 parent 96175a6 commit 412ab14

6 files changed

Lines changed: 318 additions & 48 deletions

File tree

go/cli/commands/build.go

Lines changed: 145 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,80 +30,121 @@ import (
3030

3131
//go:embed embed/Dockerfile
3232
var f embed.FS
33+
var execCmdFn = execCmd
34+
var execLookPathFn = exec.LookPath
3335

3436
const (
35-
ImageTag = "function:latest"
37+
// Builder Type
38+
Ko = "ko"
39+
Docker = "docker"
40+
41+
// Docker constant variables
42+
Image = "function:latest"
3643
DockerfilePath = "Dockerfile"
37-
builtinDockerfilePath = "embed/Dockerfile"
44+
BuiltinDockerfilePath = "embed/Dockerfile"
45+
46+
// Ko constant variables
47+
KoDockerRepoEnvVar = "KO_DOCKER_REPO"
48+
KoLocalRepo = "ko.local"
3849
)
3950

40-
func NewBuildCmd(ctx context.Context) *cobra.Command {
51+
func NewBuildRunner(ctx context.Context) *BuildRunner {
4152
r := &BuildRunner{
42-
ctx: ctx,
53+
ctx: ctx,
54+
Ko: &KoBuilder{},
55+
Docker: &DockerBuilder{},
4356
}
4457
r.Command = &cobra.Command{
45-
Use: "build",
46-
Short: "build the KRM function as a docker image",
47-
PreRunE: r.PreRunE,
48-
RunE: r.RunE,
49-
}
50-
r.Command.Flags().StringVarP(&r.Tag, "tag", "t", ImageTag,
51-
"the docker image tag")
52-
r.Command.Flags().StringVarP(&r.DockerfilePath, "file", "f", "",
53-
"Name of the Dockerfile. If not given, using a default builtin Dockerfile")
54-
return r.Command
58+
Use: "build",
59+
Short: "build your KRM function to a container image",
60+
RunE: r.RunE,
61+
}
62+
r.Command.Flags().StringVarP(&r.BuilderType, "builder", "b", Ko,
63+
"the image builder. `ko` is the default builder, which requires `go build`; `docker` is accepted, and "+
64+
" requires you to have docker installed and running")
65+
r.Command.Flags().StringVarP(&r.Docker.Image, "image", "i", Image,
66+
fmt.Sprintf("the image (with tag), default to %v", Image))
67+
r.Command.Flags().StringVarP(&r.Docker.DockerfilePath, "dockerfile", "f", "",
68+
"path to the Dockerfile. If not given, using a default builtin Dockerfile")
69+
r.Command.Flags().StringVarP(&r.Ko.Repo, "repo", "r", "",
70+
"the image repo. default to ko.local")
71+
r.Command.Flags().StringVarP(&r.Ko.Tag, "tag", "t", "latest",
72+
"the ko image tag")
73+
// TODO: Docker CLI uses `--tag` flag to refer to "image:tag", which could be confusing but broadly accepted.
74+
// We should better guide users on how to use "tag" and "image" flags for kfn.
75+
// Here we use "tag" for ko <tag> (same as `ko build --tag`) and "image" for docker <image:tag> (same as `docker build --tag`)
76+
return r
5577
}
5678

5779
type BuildRunner struct {
5880
ctx context.Context
5981
Command *cobra.Command
6082

61-
Tag string
83+
BuilderType string
84+
Tag string
85+
Ko *KoBuilder
86+
Docker *DockerBuilder
87+
}
88+
89+
type Builder interface {
90+
Build() error
91+
Validate() error
92+
}
93+
94+
type DockerBuilder struct {
95+
Image string
6296
DockerfilePath string
6397
}
6498

65-
func (r *BuildRunner) PreRunE(cmd *cobra.Command, args []string) error {
66-
if err := r.requireDocker(); err != nil {
67-
return err
68-
}
69-
if !r.dockerfileExist() {
70-
err := r.createDockerfile()
71-
if err != nil {
72-
return err
73-
}
74-
}
75-
return nil
99+
type KoBuilder struct {
100+
Repo string
101+
Tag string
76102
}
77103

78104
func (r *BuildRunner) RunE(cmd *cobra.Command, args []string) error {
79-
return r.runDockerBuild()
105+
var builder Builder
106+
switch r.BuilderType {
107+
case Docker:
108+
builder = r.Docker
109+
case Ko:
110+
builder = r.Ko
111+
}
112+
if err := builder.Validate(); err != nil {
113+
return err
114+
}
115+
return builder.Build()
80116
}
81117

82-
func (r *BuildRunner) runDockerBuild() error {
83-
args := []string{"build", ".", "-f", r.DockerfilePath, "--tag", r.Tag}
84-
cmd := exec.Command("docker", args...)
85-
var out, errout bytes.Buffer
86-
cmd.Stdout = &out
87-
cmd.Stderr = &errout
88-
err := cmd.Run()
118+
func (r *DockerBuilder) Build() error {
119+
args := []string{"build", ".", "-f", r.DockerfilePath, "--tag", r.Image}
120+
out, err := execCmdFn(nil, "docker", args...)
89121
if err != nil {
90-
color.Red(strings.TrimSpace(errout.String()))
91122
return err
92123
}
93-
color.Green(out.String())
94-
color.Green("Image %v builds successfully. Now you can publish the image", r.Tag)
124+
fmt.Println(out)
125+
color.Green("Image %v builds successfully. Now you can publish the image", r.Image)
95126
return nil
96127
}
97128

98-
func (r *BuildRunner) requireDocker() error {
99-
_, err := exec.LookPath("docker")
129+
func (r *DockerBuilder) Validate() error {
130+
if err := r.validateDockerInstalled(); err != nil {
131+
return err
132+
}
133+
if r.fileExist() {
134+
return nil
135+
}
136+
return r.createDockerfile()
137+
}
138+
139+
func (r *DockerBuilder) validateDockerInstalled() error {
140+
_, err := execLookPathFn("docker")
100141
if err != nil {
101142
return fmt.Errorf("kfn requires that `docker` is installed and on the PATH")
102143
}
103144
return nil
104145
}
105146

106-
func (r *BuildRunner) dockerfileExist() bool {
147+
func (r *DockerBuilder) fileExist() bool {
107148
if r.DockerfilePath == "" {
108149
r.DockerfilePath = DockerfilePath
109150
}
@@ -114,14 +155,74 @@ func (r *BuildRunner) dockerfileExist() bool {
114155
return true
115156
}
116157

117-
func (r *BuildRunner) createDockerfile() error {
118-
dockerfileContent, err := f.ReadFile(builtinDockerfilePath)
158+
func (r *DockerBuilder) createDockerfile() error {
159+
dockerfileContent, err := f.ReadFile(BuiltinDockerfilePath)
160+
if err != nil {
161+
return err
162+
}
163+
if err = os.WriteFile(DockerfilePath, dockerfileContent, 0644); err != nil {
164+
return err
165+
}
166+
fmt.Println("created Dockerfile")
167+
return nil
168+
}
169+
170+
func (r *KoBuilder) GuaranteeKoInstalled() error {
171+
_, err := execLookPathFn("ko")
172+
if err == nil {
173+
return nil
174+
}
175+
if _, err = execCmdFn(nil, "go", "install", "github.com/google/ko@latest"); err != nil {
176+
return err
177+
}
178+
fmt.Println("successfully installed ko")
179+
return nil
180+
}
181+
func (r *KoBuilder) Build() error {
182+
args := []string{"build", "-B", "--tags", r.Tag}
183+
envs := []string{KoDockerRepoEnvVar + "=" + r.Repo}
184+
out, err := execCmdFn(envs, "ko", args...)
119185
if err != nil {
120186
return err
121187
}
122-
if err := os.WriteFile(DockerfilePath, dockerfileContent, 0644); err != nil {
188+
189+
if r.Repo == KoLocalRepo {
190+
color.Green("Image builds successfully. Now you can publish the image %v", out)
191+
} else {
192+
color.Green("Image builds and pushed successfully: %v", out)
193+
}
194+
return nil
195+
}
196+
197+
func (r *KoBuilder) Validate() error {
198+
if err := r.GuaranteeKoInstalled(); err != nil {
123199
return err
124200
}
125-
color.Green("created Dockerfile")
201+
// Find KO_DOCKER_REPO value from multiple places for `ko build`.
202+
if r.Repo != "" {
203+
return nil
204+
}
205+
if repo, ok := os.LookupEnv(KoDockerRepoEnvVar); ok {
206+
r.Repo = repo
207+
return nil
208+
}
209+
r.Repo = "ko.local"
126210
return nil
127211
}
212+
213+
func execCmd(envs []string, name string, args ...string) (string, error) {
214+
cmd := exec.Command(name, args...)
215+
if len(envs) != 0 {
216+
cmd.Env = os.Environ()
217+
cmd.Env = append(cmd.Env, envs...)
218+
}
219+
var out, errout bytes.Buffer
220+
cmd.Stdout = &out
221+
cmd.Stderr = &errout
222+
err := cmd.Run()
223+
if err != nil {
224+
color.Red(strings.TrimSpace(errout.String()))
225+
return "", err
226+
}
227+
return strings.TrimSpace(out.String()), nil
228+
}

go/cli/commands/build_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package commands
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestBuild(t *testing.T) {
27+
testcases := map[string]struct {
28+
args []string
29+
// expected args is key, and expected return is value
30+
cmdExpected []string
31+
// expected env var is key, and existence is value
32+
lookPathExpected map[string]bool
33+
expectedError string
34+
}{
35+
"default build is ko, ko already exists": {
36+
args: []string{""},
37+
cmdExpected: []string{
38+
"KO_DOCKER_REPO=ko.local ko build -B --tags latest",
39+
},
40+
lookPathExpected: map[string]bool{
41+
"ko": true,
42+
},
43+
},
44+
"default build is ko, ko not exists": {
45+
args: []string{""},
46+
cmdExpected: []string{
47+
"go install github.com/google/ko@latest",
48+
"KO_DOCKER_REPO=ko.local ko build -B --tags latest",
49+
},
50+
lookPathExpected: map[string]bool{
51+
"ko": false,
52+
},
53+
},
54+
"ko as builder, specify repo": {
55+
args: []string{"--repo=gcr.io/test"},
56+
cmdExpected: []string{
57+
"KO_DOCKER_REPO=gcr.io/test ko build -B --tags latest",
58+
},
59+
lookPathExpected: map[string]bool{
60+
"ko": true,
61+
},
62+
},
63+
"ko as builder, specify tag": {
64+
args: []string{"--repo=gcr.io/test", "--tag=v1"},
65+
cmdExpected: []string{
66+
"KO_DOCKER_REPO=gcr.io/test ko build -B --tags v1",
67+
},
68+
lookPathExpected: map[string]bool{
69+
"ko": true,
70+
},
71+
},
72+
"docker as builder, docker not exists": {
73+
args: []string{"--builder=docker"},
74+
lookPathExpected: map[string]bool{
75+
"docker": false,
76+
},
77+
expectedError: "kfn requires that `docker` is installed and on the PATH",
78+
},
79+
"docker as builder, docker exists": {
80+
args: []string{"--builder=docker"},
81+
cmdExpected: []string{
82+
"docker build . -f Dockerfile --tag function:latest",
83+
},
84+
lookPathExpected: map[string]bool{
85+
"docker": true,
86+
},
87+
},
88+
"docker as builder, specify dockerfile": {
89+
args: []string{"--builder=docker", "--dockerfile=tmp/Dockerfile"},
90+
cmdExpected: []string{
91+
"docker build . -f tmp/Dockerfile --tag function:latest",
92+
},
93+
lookPathExpected: map[string]bool{
94+
"docker": true,
95+
},
96+
},
97+
"docker as builder, specify image": {
98+
args: []string{"--builder=docker", "--image=dockertest:latest", "--dockerfile=tmp/Dockerfile"},
99+
cmdExpected: []string{
100+
"docker build . -f tmp/Dockerfile --tag dockertest:latest",
101+
},
102+
lookPathExpected: map[string]bool{
103+
"docker": true,
104+
},
105+
},
106+
}
107+
for name, test := range testcases {
108+
r := NewBuildRunner(context.TODO())
109+
execCmdFn = func(envs []string, name string, args ...string) (string, error) {
110+
fakeExecCmd(t, test.cmdExpected, envs, name, args...)
111+
return "", nil
112+
}
113+
execLookPathFn = func(file string) (string, error) {
114+
return "", fakeExecLookPath(t, test.lookPathExpected, file)
115+
}
116+
r.Command.SetArgs(test.args)
117+
err := r.Command.Execute()
118+
if test.expectedError == "" {
119+
if err != nil {
120+
t.Errorf("%v failed. got error: %v", name, err)
121+
}
122+
} else {
123+
assert.EqualError(t, err, test.expectedError)
124+
}
125+
}
126+
}
127+
128+
func fakeExecCmd(t *testing.T, expectedArgsAndReturns []string, envs []string, name string, args ...string) {
129+
c := envs
130+
c = append(c, name)
131+
c = append(c, args...)
132+
command := strings.Join(c, " ")
133+
for _, expected := range expectedArgsAndReturns {
134+
if expected == command {
135+
return
136+
}
137+
}
138+
t.Fatalf("unexpected command run %v", command)
139+
}
140+
141+
func fakeExecLookPath(t *testing.T, expectedlookPath map[string]bool, name string) error {
142+
val, ok := expectedlookPath[name]
143+
if !ok {
144+
t.Fatalf("unexpected env var check %v", name)
145+
}
146+
if val {
147+
return nil
148+
}
149+
return fmt.Errorf("env var not exists")
150+
}

0 commit comments

Comments
 (0)