Skip to content

Commit 294dfed

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

6 files changed

Lines changed: 300 additions & 44 deletions

File tree

go/cli/commands/build.go

Lines changed: 141 additions & 40 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{
4253
ctx: ctx,
54+
Ko: &KoBuilder{},
55+
Docker: &DockerBuilder{},
4356
}
4457
r.Command = &cobra.Command{
4558
Use: "build",
46-
Short: "build the KRM function as a docker image",
47-
PreRunE: r.PreRunE,
59+
Short: "build your KRM function to a container image",
4860
RunE: r.RunE,
4961
}
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
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: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestBuild(t *testing.T) {
13+
testcases := map[string]struct{
14+
args []string
15+
// expected args is key, and expected return is value
16+
cmdExpected []string
17+
// expected env var is key, and existence is value
18+
lookPathExpected map[string]bool
19+
expectedError string
20+
}{
21+
"default build is ko, ko already exists": {
22+
args: []string{""},
23+
cmdExpected: []string{
24+
"KO_DOCKER_REPO=ko.local ko build -B --tags latest",
25+
},
26+
lookPathExpected: map[string]bool{
27+
"ko": true,
28+
},
29+
},
30+
"default build is ko, ko not exists": {
31+
args: []string{""},
32+
cmdExpected: []string{
33+
"go install github.com/google/ko@latest",
34+
"KO_DOCKER_REPO=ko.local ko build -B --tags latest",
35+
},
36+
lookPathExpected: map[string]bool{
37+
"ko": false,
38+
},
39+
},
40+
"ko as builder, specify repo": {
41+
args: []string{"--repo=gcr.io/test"},
42+
cmdExpected: []string{
43+
"KO_DOCKER_REPO=gcr.io/test ko build -B --tags latest",
44+
},
45+
lookPathExpected: map[string]bool{
46+
"ko": true,
47+
},
48+
},
49+
"ko as builder, specify tag": {
50+
args: []string{"--repo=gcr.io/test", "--tag=v1"},
51+
cmdExpected: []string{
52+
"KO_DOCKER_REPO=gcr.io/test ko build -B --tags v1",
53+
},
54+
lookPathExpected: map[string]bool{
55+
"ko": true,
56+
},
57+
},
58+
"docker as builder, docker not exists": {
59+
args: []string{"--builder=docker"},
60+
lookPathExpected: map[string]bool{
61+
"docker": false,
62+
},
63+
expectedError: "kfn requires that `docker` is installed and on the PATH",
64+
},
65+
"docker as builder, docker exists": {
66+
args: []string{"--builder=docker"},
67+
cmdExpected: []string{
68+
"docker build . -f Dockerfile --tag function:latest",
69+
},
70+
lookPathExpected: map[string]bool{
71+
"docker": true,
72+
},
73+
},
74+
"docker as builder, specify dockerfile": {
75+
args: []string{"--builder=docker", "--dockerfile=tmp/Dockerfile"},
76+
cmdExpected: []string{
77+
"docker build . -f tmp/Dockerfile --tag function:latest",
78+
},
79+
lookPathExpected: map[string]bool{
80+
"docker": true,
81+
},
82+
},
83+
"docker as builder, specify image": {
84+
args: []string{"--builder=docker", "--image=dockertest:latest", "--dockerfile=tmp/Dockerfile"},
85+
cmdExpected: []string{
86+
"docker build . -f tmp/Dockerfile --tag dockertest:latest",
87+
},
88+
lookPathExpected: map[string]bool{
89+
"docker": true,
90+
},
91+
},
92+
}
93+
for name, test := range testcases{
94+
r := NewBuildRunner(context.TODO())
95+
execCmdFn = func(envs []string, name string, args... string) (string, error) {
96+
fakeExecCmd(t, test.cmdExpected, envs, name, args...)
97+
return "", nil
98+
}
99+
execLookPathFn = func(file string) (string, error) {
100+
return "", fakeExecLookPath(t, test.lookPathExpected, file)
101+
}
102+
r.Command.SetArgs(test.args)
103+
err := r.Command.Execute()
104+
if test.expectedError == "" {
105+
if err != nil {
106+
t.Errorf("%v failed. got error: %v", name, err)
107+
}
108+
} else {
109+
assert.EqualError(t, err, test.expectedError)
110+
}
111+
}
112+
}
113+
114+
func fakeExecCmd(t *testing.T, expectedArgsAndReturns []string, envs []string, name string, args... string) {
115+
c := envs
116+
c = append(c, name)
117+
c = append(c, args...)
118+
command := strings.Join(c, " ")
119+
for _, expected:= range expectedArgsAndReturns {
120+
if expected == command {
121+
return
122+
}
123+
}
124+
t.Fatalf("unexpected command run %v", command)
125+
}
126+
127+
func fakeExecLookPath(t *testing.T, expectedlookPath map[string]bool, name string) error {
128+
val, ok := expectedlookPath[name]
129+
if !ok {
130+
t.Fatalf("unexpected env var check %v", name)
131+
}
132+
if val {
133+
return nil
134+
}
135+
return fmt.Errorf("env var not exists")
136+
}

go/cli/commands/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const (
2929
DefaultPkgName = "function"
3030
)
3131

32-
func NewInitCmd(ctx context.Context) *cobra.Command {
32+
func NewInitRunner(ctx context.Context) *InitRunner {
3333
r := &InitRunner{
3434
ctx: ctx,
3535
}
@@ -41,7 +41,7 @@ func NewInitCmd(ctx context.Context) *cobra.Command {
4141
}
4242
r.Command.Flags().StringVarP(&r.FnPkgPath, "fnPkg", "", DefaultFnPkg,
4343
"a kpt package that contains a basic KRM function source code to get start")
44-
return r.Command
44+
return r
4545
}
4646

4747
// InitRunner initializes a KRM function project from a scaffolded `kpt pkg`.

0 commit comments

Comments
 (0)