From b46fa5372d2ce790f0c03ba19d5e103773d28ebc Mon Sep 17 00:00:00 2001 From: Michael Whittaker Date: Wed, 25 Oct 2023 22:04:01 +0000 Subject: [PATCH] Refactored code that builds Docker containers. 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. --- internal/impl/deploy.go | 3 +- internal/impl/docker.go | 203 +++++++++++++++++++--------------------- 2 files changed, 99 insertions(+), 107 deletions(-) diff --git a/internal/impl/deploy.go b/internal/impl/deploy.go index b5bdcbf..d1a61cc 100644 --- a/internal/impl/deploy.go +++ b/internal/impl/deploy.go @@ -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 } diff --git a/internal/impl/docker.go b/internal/impl/docker.go index cdc46f0..49f4b0d 100644 --- a/internal/impl/docker.go +++ b/internal/impl/docker.go @@ -27,79 +27,85 @@ 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"] -`)) - -// 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 } - toCopy := []string{app.Binary} - var toInstall []string + tool, err := os.Executable() + if err != nil { + return "", err + } + 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 @@ -108,70 +114,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. + 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, time.Second*120) + 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) @@ -180,7 +171,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))