Skip to content

Commit 5d8cfb0

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

File tree

6 files changed

+326
-50
lines changed

6 files changed

+326
-50
lines changed

go/cli/commands/build.go

Lines changed: 150 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,95 +15,133 @@
1515
package commands
1616

1717
import (
18-
"bytes"
1918
"context"
2019
"embed"
2120
"errors"
2221
"fmt"
2322
"os"
2423
"os/exec"
25-
"strings"
2624

2725
"github.com/heroku/color"
2826
"github.com/spf13/cobra"
2927
)
3028

3129
//go:embed embed/Dockerfile
3230
var f embed.FS
31+
var execCmdFn = execCmd
32+
var execLookPathFn = exec.LookPath
3333

3434
const (
35-
ImageTag = "function:latest"
35+
// Builder Type
36+
Ko = "ko"
37+
Docker = "docker"
38+
39+
// Docker constant variables
40+
Image = "function:latest"
3641
DockerfilePath = "Dockerfile"
37-
builtinDockerfilePath = "embed/Dockerfile"
42+
BuiltinDockerfilePath = "embed/Dockerfile"
43+
44+
// Ko constant variables
45+
KoDockerRepoEnvVar = "KO_DOCKER_REPO"
46+
KoLocalRepo = "ko.local"
3847
)
3948

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

5777
type BuildRunner struct {
5878
ctx context.Context
5979
Command *cobra.Command
6080

61-
Tag string
81+
BuilderType string
82+
Tag string
83+
Ko *KoBuilder
84+
Docker *DockerBuilder
85+
}
86+
87+
type Builder interface {
88+
Build() error
89+
Validate() error
90+
}
91+
92+
type DockerBuilder struct {
93+
Image string
6294
DockerfilePath string
6395
}
6496

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
97+
type KoBuilder struct {
98+
Repo string
99+
Tag string
76100
}
77101

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

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()
116+
func (r *DockerBuilder) Build() error {
117+
args := []string{"build", ".", "-f", r.DockerfilePath, "--tag", r.Image}
118+
err := execCmdFn(nil, "docker", args...)
89119
if err != nil {
90-
color.Red(strings.TrimSpace(errout.String()))
91120
return err
92121
}
93-
color.Green(out.String())
94-
color.Green("Image %v builds successfully. Now you can publish the image", r.Tag)
122+
color.Green("Image %v built successfully. Now you can publish the image", r.Image)
95123
return nil
96124
}
97125

98-
func (r *BuildRunner) requireDocker() error {
99-
_, err := exec.LookPath("docker")
126+
func (r *DockerBuilder) Validate() error {
127+
if err := r.validateDockerInstalled(); err != nil {
128+
return err
129+
}
130+
if r.dockerfileExists() {
131+
return nil
132+
}
133+
return r.createDockerfile()
134+
}
135+
136+
func (r *DockerBuilder) validateDockerInstalled() error {
137+
_, err := execLookPathFn("docker")
100138
if err != nil {
101139
return fmt.Errorf("kfn requires that `docker` is installed and on the PATH")
102140
}
103141
return nil
104142
}
105143

106-
func (r *BuildRunner) dockerfileExist() bool {
144+
func (r *DockerBuilder) dockerfileExists() bool {
107145
if r.DockerfilePath == "" {
108146
r.DockerfilePath = DockerfilePath
109147
}
@@ -114,14 +152,80 @@ func (r *BuildRunner) dockerfileExist() bool {
114152
return true
115153
}
116154

117-
func (r *BuildRunner) createDockerfile() error {
118-
dockerfileContent, err := f.ReadFile(builtinDockerfilePath)
155+
func (r *DockerBuilder) createDockerfile() error {
156+
dockerfileContent, err := f.ReadFile(BuiltinDockerfilePath)
157+
if err != nil {
158+
return err
159+
}
160+
if err = os.WriteFile(DockerfilePath, dockerfileContent, 0644); err != nil {
161+
return err
162+
}
163+
fmt.Println("created Dockerfile")
164+
return nil
165+
}
166+
167+
func (r *KoBuilder) GuaranteeKoInstalled() error {
168+
_, err := execLookPathFn("ko")
169+
if err == nil {
170+
return nil
171+
}
172+
gobin := os.Getenv("GOBIN")
173+
if gobin == "" && os.Getenv("GOPATH") != ""{
174+
gobin = os.Getenv("GOPATH") + "/bin"
175+
}
176+
if gobin == "" && os.Getenv("HOME") != "" {
177+
gobin = os.Getenv("HOME") + "/go/bin"
178+
}
179+
var envs []string
180+
if gobin != "" {
181+
envs =[]string{"GOBIN" + "=" + gobin}
182+
}
183+
if err = execCmdFn(envs, "go", "install", "github.com/google/ko@latest"); err != nil {
184+
return err
185+
}
186+
fmt.Println("successfully installed ko")
187+
return nil
188+
}
189+
func (r *KoBuilder) Build() error {
190+
args := []string{"build", "-B", "--tags", r.Tag}
191+
envs := []string{KoDockerRepoEnvVar + "=" + r.Repo}
192+
err := execCmdFn(envs, "ko", args...)
119193
if err != nil {
120194
return err
121195
}
122-
if err := os.WriteFile(DockerfilePath, dockerfileContent, 0644); err != nil {
196+
197+
if r.Repo == KoLocalRepo {
198+
color.Green("Image built successfully. Now you can publish the image")
199+
} else {
200+
color.Green("Image built and pushed successfully")
201+
}
202+
return nil
203+
}
204+
205+
func (r *KoBuilder) Validate() error {
206+
if err := r.GuaranteeKoInstalled(); err != nil {
123207
return err
124208
}
125-
color.Green("created Dockerfile")
209+
// Find KO_DOCKER_REPO value from multiple places for `ko build`.
210+
if r.Repo != "" {
211+
return nil
212+
}
213+
if repo, ok := os.LookupEnv(KoDockerRepoEnvVar); ok {
214+
r.Repo = repo
215+
return nil
216+
}
217+
r.Repo = "ko.local"
126218
return nil
127219
}
220+
221+
func execCmd(envs []string, name string, args ...string) error {
222+
cmd := exec.Command(name, args...)
223+
if len(envs) != 0 {
224+
cmd.Env = os.Environ()
225+
cmd.Env = append(cmd.Env, envs...)
226+
}
227+
cmd.Stdout = os.Stdout
228+
cmd.Stderr = os.Stderr
229+
err := cmd.Run()
230+
return err
231+
}

0 commit comments

Comments
 (0)