Skip to content

Commit

Permalink
Refactored code that builds Docker containers. (#85)
Browse files Browse the repository at this point in the history
This PR refactors the code in `docker.go` that builds Docker containers.
I'm working on introducing a very simple plugin system to weaver-kube.
To register plugins, users have to write a custom "weaver-kube" binary,
which might be called something other than "weaver-kube". `docker.go`
currently assumes the tool is called "weaver-kube". This PR refactors
things to allow for a "weaver-kube" binary of any name.
  • Loading branch information
mwhittaker authored Nov 6, 2023
1 parent b03aba6 commit 228522a
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 106 deletions.
3 changes: 2 additions & 1 deletion internal/impl/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func Deploy(ctx context.Context, configFilename string) error {
depId := uuid.New().String()

// Build the docker image for the deployment.
image, err := buildAndUploadDockerImage(ctx, app, depId, config.Image, config.Repo)
opts := dockerOptions{image: config.Image, repo: config.Repo}
image, err := buildAndUploadDockerImage(ctx, app, depId, opts)
if err != nil {
return err
}
Expand Down
204 changes: 99 additions & 105 deletions internal/impl/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,79 +27,88 @@ import (
"time"

"github.com/ServiceWeaver/weaver/runtime/protos"
"github.com/google/uuid"
)

// dockerfileTmpl contains the templatized content of the Dockerfile.
//
// TODO(rgrandl): See if we can use a much simpler image. Previously we've been
// using gcr.io/distroless/base-debian11, but it lacks libraries that can lead to
// runtime errors (e.g., glibc).
var dockerfileTmpl = template.Must(template.New("Dockerfile").Parse(`
{{if . }}
FROM golang:bullseye as builder
RUN echo ""{{range .}} && go install {{.}}{{end}}
{{end}}
FROM ubuntu:rolling
WORKDIR /weaver/
COPY . .
{{if . }}
COPY --from=builder /go/bin/ /weaver/
{{end}}
ENTRYPOINT ["/weaver/weaver-kube"]
`))
// The maximum time to wait for `docker build` to finish before aborting.
const dockerBuildTimeout = time.Second * 120

// buildSpec holds information about a container image build.
type buildSpec struct {
image string // container image name
files []string // files that should be copied to the container
goInstall []string // binary targets that should be 'go install'-ed
// dockerOptions configure how Docker images are built and pushed.
type dockerOptions struct {
image string // see kubeConfig.Image
repo string // see kubeConfig.Repo
}

// buildAndUploadDockerImage builds a docker image and uploads it to a remote
// buildAndUploadDockerImage builds a Docker image and uploads it to a remote
// repo, if one is specified. It returns the image name that should be used in
// Kubernetes YAML files.
func buildAndUploadDockerImage(ctx context.Context, app *protos.AppConfig, depId string,
image, repo string) (string, error) {
// Create the build specifications.
spec, err := dockerBuildSpec(app, depId, image)
func buildAndUploadDockerImage(ctx context.Context, app *protos.AppConfig, depId string, opts dockerOptions) (string, error) {
// Build the Docker image.
image, err := buildImage(ctx, app, depId, opts)
if err != nil {
return "", fmt.Errorf("unable to build image spec: %w", err)
}

// Build the docker image.
if err := buildImage(ctx, spec); err != nil {
return "", fmt.Errorf("unable to create image: %w", err)
}
if opts.repo == "" {
return image, nil
}

image = spec.image
if repo != "" {
// Push the docker image to the repo.
if image, err = pushImage(ctx, image, repo); err != nil {
return "", fmt.Errorf("unable to push image: %w", err)
}
// Push the Docker image to the repo.
image, err = pushImage(ctx, image, opts.repo)
if err != nil {
return "", fmt.Errorf("unable to push image: %w", err)
}
return image, nil
}

// dockerBuildSpec creates a build specification for an app deployment.
func dockerBuildSpec(app *protos.AppConfig, depId string, image string) (*buildSpec, error) {
// Figure out which tool binary will run inside the container.
// buildImage builds a Docker image.
func buildImage(ctx context.Context, app *protos.AppConfig, depId string, opts dockerOptions) (string, error) {
// Pick an image name.
image := opts.image
if image == "" {
image = fmt.Sprintf("%s:%s", app.Name, depId[:8])
}
fmt.Fprintf(os.Stderr, greenText(), fmt.Sprintf("Building image %s...", image))

// Create:
// workDir/
// file1
// file2
// ...
// fileN
// Dockerfile - Docker build instructions

// Create workDir/.
workDir, err := os.MkdirTemp("", "weaver-kube")
if err != nil {
return "", err
}
defer os.RemoveAll(workDir)

// Copy the application binary to workDir/.
if err := cp(app.Binary, filepath.Join(workDir, filepath.Base(app.Binary))); err != nil {
return "", err
}

// Copy the "weaver-kube" binary into workDir/ if the binary can run in the
// container. Otherwise, we'll install the "weaver-kube" binary inside the
// container.
toolVersion, toolIsDev, err := ToolVersion()
if err != nil {
return nil, err
return "", err
}
tool, err := os.Executable()
if err != nil {
return "", err
}
toCopy := []string{app.Binary}
var toInstall []string
install := "" // "weaver-kube" binary to install, if any
if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
// The running tool binary can run inside the container: copy it.
toolBinPath, err := os.Executable()
if err != nil {
return nil, err
// The "weaver-kube" binary can run inside the container, so copy it.
if err := cp(tool, filepath.Join(workDir, filepath.Base(tool))); err != nil {
return "", err
}
toCopy = append(toCopy, toolBinPath)
} else if toolIsDev {
// Devel tool binary that's not linux/amd64: prompt the user.
// The "weaver-kube" binary has local modifications, but it cannot be
// copied into the container. In this case, we install the latest
// version of "weaver-kube" in the container, if approved by the user.
scanner := bufio.NewScanner(os.Stdin)
fmt.Print(
`The running weaver-kube binary hasn't been cross-compiled for linux/amd64 and
Expand All @@ -108,70 +117,55 @@ downloaded and installed in the container. Do you want to proceed? [Y/n] `)
scanner.Scan()
text := scanner.Text()
if text != "" && text != "y" && text != "Y" {
return nil, fmt.Errorf("user bailed out")
return "", fmt.Errorf("user bailed out")
}
toInstall = append(toInstall, "github.com/ServiceWeaver/weaver-kube/cmd/weaver-kube@latest")
install = "github.com/ServiceWeaver/weaver-kube/cmd/weaver-kube@latest"
} else {
// Released tool binary that's not compiled to linux/amd64. Re-install
// it inside the container.
toInstall = append(toInstall, "github.com/ServiceWeaver/weaver-kube/cmd/weaver-kube@"+toolVersion)
}

if image == "" {
image = fmt.Sprintf("%s:%s", app.Name, depId[:8])
}

return &buildSpec{
image: image,
files: toCopy,
goInstall: toInstall,
}, nil
}

// buildImage builds a docker image with a given spec.
func buildImage(ctx context.Context, spec *buildSpec) error {
fmt.Fprintf(os.Stderr, greenText(), fmt.Sprintf("Building image %s...", spec.image))
// Create:
// workDir/
// file1
// file2
// ...
// fileN
// Dockerfile - docker build instructions
ctx, cancel := context.WithTimeout(ctx, time.Second*120)
defer cancel()

// Create workDir/.
workDir := filepath.Join(os.TempDir(), fmt.Sprintf("weaver%s", uuid.New().String()))
if err := os.Mkdir(workDir, 0o700); err != nil {
return err
// Install the currently running version of "weaver-kube" in the
// container.
install = "github.com/ServiceWeaver/weaver-kube/cmd/weaver-kube@" + toolVersion
}
defer os.RemoveAll(workDir)

// Copy the files from spec.files to workDir/.
for _, file := range spec.files {
workDirFile := filepath.Join(workDir, filepath.Base(filepath.Clean(file)))
if err := cp(file, workDirFile); err != nil {
return err
}
// Create a Dockerfile in workDir/.
type content struct {
Install string // "weaver-kube" binary to install, if any
Entrypoint string // container entrypoint
}
var template = template.Must(template.New("Dockerfile").Parse(`
{{if .Install }}
FROM golang:bullseye as builder
RUN go install "{{.Install}}"
{{end}}
// Create a Dockerfile in workDir/.
dockerFile, err := os.Create(filepath.Join(workDir, dockerfileTmpl.Name()))
FROM ubuntu:rolling
WORKDIR /weaver/
COPY . .
{{if .Install }}
COPY --from=builder /go/bin/ /weaver/
{{end}}
ENTRYPOINT ["{{.Entrypoint}}"]
`))
dockerFile, err := os.Create(filepath.Join(workDir, "Dockerfile"))
if err != nil {
return err
return "", err
}
if err := dockerfileTmpl.Execute(dockerFile, spec.goInstall); err != nil {
dockerFile.Close()
return err
defer dockerFile.Close()
c := content{Install: install}
if install != "" {
c.Entrypoint = "/weaver/weaver-kube"
} else {
c.Entrypoint = filepath.Join("/weaver", filepath.Base(tool))
}
if err := dockerFile.Close(); err != nil {
return err
if err := template.Execute(dockerFile, c); err != nil {
return "", err
}
return dockerBuild(ctx, workDir, spec.image)

ctx, cancel := context.WithTimeout(ctx, dockerBuildTimeout)
defer cancel()
return image, dockerBuild(ctx, workDir, image)
}

// dockerBuild builds a docker image given a directory and an image name.
// dockerBuild builds a Docker image given a directory and an image name.
func dockerBuild(ctx context.Context, dir, image string) error {
fmt.Fprintln(os.Stderr, "Building image ", image)
c := exec.CommandContext(ctx, "docker", "build", dir, "-t", image)
Expand All @@ -180,7 +174,7 @@ func dockerBuild(ctx context.Context, dir, image string) error {
return c.Run()
}

// pushImage pushes the provided docker image to the provided repo, returning
// pushImage pushes the provided Docker image to the provided repo, returning
// the repo-qualified image name.
func pushImage(ctx context.Context, image, repo string) (string, error) {
fmt.Fprintf(os.Stderr, greenText(), fmt.Sprintf("\nUploading image to %s...", repo))
Expand Down

0 comments on commit 228522a

Please sign in to comment.