From 5140a9a9022154018dc01c7dfb52956a817f6814 Mon Sep 17 00:00:00 2001
From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com>
Date: Fri, 12 Apr 2024 20:52:30 +0530
Subject: [PATCH] Add docker images for the CLI (#1353)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Changes
This PR makes changes to support creating a docker image for the CLI
with the `terraform` dependencies built in. This is useful for customers
that operate in a network-restricted environment. Normally DABs makes
API calls to registry.terraform.io to setup the terraform dependencies,
with this setup the CLI/DABs will rely on the provider binaries bundled
in the docker image.
### Specifically this PR makes the following changes:
----------------
Modifies the CLI release workflow to publish the docker images in the
Github Container Registry. URL:
https://github.com/databricks/cli/pkgs/container/cli.
We use docker support in `goreleaser` to build and publish the images.
Using goreleaser ensures the CLI packaged in the docker image is the
same release artifact as the normal releases. For more information see:
1. https://goreleaser.com/cookbooks/multi-platform-docker-images
2. https://goreleaser.com/customization/docker/
Other choices made include:
1. Using `alpine` as the base image. The reason is `alpine` is a small
and lightweight linux distribution (~5MB) and an industry standard.
2. Not using [docker
manifest](https://docs.docker.com/reference/cli/docker/manifest) to
create a multi-arch build. This is because the functionality is still
experimental.
------------------
Make the `DATABRICKS_TF_VERSION` and `DATABRICKS_TF_PROVIDER_VERSION`
environment variables optional for using the terraform file mirror.
While it's not strictly necessary to make the docker image work, it's
the "right" behaviour and reduces complexity. The rationale is:
- These environment variables here are needed so the Databricks CLI does
not accidentally use the file mirror bundled with VSCode if it's
incompatible. This does not require the env vars to be mandatory.
context: https://github.com/databricks/cli/pull/1294
- This makes the `Dockerfile` and `setup.sh` simpler. We don't need an
[entrypoint.sh script to set the version environment
variables](https://medium.com/@leonardo5621_66451/learn-how-to-use-entrypoint-scripts-in-docker-images-fede010f172d).
This also makes using an interactive terminal with `docker run -it ...`
work out of the box.
## Tests
Tested manually.
--------------------
To test the release pipeline I triggered a couple of dummy releases and
verified that the images are built successfully and uploaded to Github.
1. https://github.com/databricks/cli/pkgs/container/cli
3. workflow for release:
https://github.com/databricks/cli/actions/runs/8646106333
--------------------
I tested the docker container itself by setting up
[Charles](https://www.charlesproxy.com/) as an HTTP proxy and verifying
that no HTTP requests are made to `registry.terraform.io`
Before:
FYI, The Charles web proxy is hosted at localhost:8888.
```
shreyas.goenka@THW32HFW6T bundle-playground % rm -r .databricks
shreyas.goenka@THW32HFW6T bundle-playground % HTTP_PROXY="http://localhost:8888" HTTPS_PROXY="http://localhost:8888" cli bundle deploy
Uploading bundle files to /Users/shreyas.goenka@databricks.com/.bundle/bundle-playground/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!
```
After:
This time bundle deploy is run from inside the docker container. We use
`host.docker.internal` to map to localhost on the host machine, and -v
to mount the host file system as a volume.
```
shreyas.goenka@THW32HFW6T bundle-playground % docker run -v ~/projects/bundle-playground:/bundle -v ~/.databrickscfg:/root/.databrickscfg -it --entrypoint /bin/sh -e HTTP_PROXY="http://host.docker.internal:8888" -e HTTPS_PROXY="http://host.docker.internal:8888" --network host ghcr.io/databricks/cli:latest-arm64
/ # cd /bundle/
/bundle # rm -r .databricks/
/bundle # databricks bundle deploy
Uploading bundle files to /Users/shreyas.goenka@databricks.com/.bundle/bundle-playground/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!
```
---
.github/workflows/release.yml | 13 +++++++
.goreleaser.yaml | 31 +++++++++++++++
Dockerfile | 24 ++++++++++++
bundle/deploy/terraform/init.go | 26 +++++++++----
bundle/deploy/terraform/init_test.go | 58 ++++++++++++++++++++++++++++
docker/config.tfrc | 6 +++
docker/setup.sh | 17 ++++++++
7 files changed, 168 insertions(+), 7 deletions(-)
create mode 100644 Dockerfile
create mode 100644 docker/config.tfrc
create mode 100755 docker/setup.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 43ceea2cdd..f9b4ec15f0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,6 +24,19 @@ jobs:
with:
go-version: 1.21.x
+ # Log into the GitHub Container Registry. The goreleaser action will create
+ # the docker images and push them to the GitHub Container Registry.
+ - uses: "docker/login-action@v3"
+ with:
+ registry: "ghcr.io"
+ username: "${{ github.actor }}"
+ password: "${{ secrets.GITHUB_TOKEN }}"
+
+ # QEMU is required to build cross platform docker images using buildx.
+ # It allows virtualization of the CPU architecture at the application level.
+ - name: Set up QEMU dependency
+ uses: docker/setup-qemu-action@v3
+
- name: Run GoReleaser
id: releaser
uses: goreleaser/goreleaser-action@v4
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 0cf87a9ce1..e440687473 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -45,6 +45,37 @@ archives:
# file name then additional logic to clean up older builds would be needed.
name_template: 'databricks_cli_{{ if not .IsSnapshot }}{{ .Version }}_{{ end }}{{ .Os }}_{{ .Arch }}'
+dockers:
+ - id: arm64
+ goarch: arm64
+ # We need to use buildx to build arm64 image on a amd64 machine.
+ use: buildx
+ image_templates:
+ # Docker tags can't have "+" in them, so we replace it with "-"
+ - 'ghcr.io/databricks/cli:{{replace .Version "+" "-"}}-arm64'
+ - 'ghcr.io/databricks/cli:latest-arm64'
+ build_flag_templates:
+ - "--build-arg=ARCH=arm64"
+ - "--platform=linux/arm64"
+ extra_files:
+ - "./docker/config.tfrc"
+ - "./docker/setup.sh"
+
+ - id: amd64
+ goarch: amd64
+ use: buildx
+ image_templates:
+ # Docker tags can't have "+" in them, so we replace it with "-"
+ - 'ghcr.io/databricks/cli:{{replace .Version "+" "-"}}-amd64'
+ - 'ghcr.io/databricks/cli:latest-amd64'
+ build_flag_templates:
+ - "--build-arg=ARCH=amd64"
+ - "--platform=linux/amd64"
+ extra_files:
+ - "./docker/config.tfrc"
+ - "./docker/setup.sh"
+
+
checksum:
name_template: 'databricks_cli_{{ .Version }}_SHA256SUMS'
algorithm: sha256
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..d4e7614c89
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+FROM alpine:3.19 as builder
+
+RUN ["apk", "add", "jq"]
+
+WORKDIR /build
+
+COPY ./docker/setup.sh /build/docker/setup.sh
+COPY ./databricks /app/databricks
+COPY ./docker/config.tfrc /app/config/config.tfrc
+
+ARG ARCH
+RUN /build/docker/setup.sh
+
+# Start from a fresh base image, to remove any build artifacts and scripts.
+FROM alpine:3.19
+
+ENV DATABRICKS_TF_EXEC_PATH "/app/bin/terraform"
+ENV DATABRICKS_TF_CLI_CONFIG_FILE "/app/config/config.tfrc"
+ENV PATH="/app:${PATH}"
+
+COPY --from=builder /app /app
+
+ENTRYPOINT ["/app/databricks"]
+CMD ["-h"]
diff --git a/bundle/deploy/terraform/init.go b/bundle/deploy/terraform/init.go
index 9f42353104..69ae70ba63 100644
--- a/bundle/deploy/terraform/init.go
+++ b/bundle/deploy/terraform/init.go
@@ -138,23 +138,35 @@ func inheritEnvVars(ctx context.Context, environ map[string]string) error {
func getEnvVarWithMatchingVersion(ctx context.Context, envVarName string, versionVarName string, currentVersion string) (string, error) {
envValue := env.Get(ctx, envVarName)
versionValue := env.Get(ctx, versionVarName)
- if envValue == "" || versionValue == "" {
- log.Debugf(ctx, "%s and %s aren't defined", envVarName, versionVarName)
- return "", nil
- }
- if versionValue != currentVersion {
- log.Debugf(ctx, "%s as %s does not match the current version %s, ignoring %s", versionVarName, versionValue, currentVersion, envVarName)
+
+ // return early if the environment variable is not set
+ if envValue == "" {
+ log.Debugf(ctx, "%s is not defined", envVarName)
return "", nil
}
+
+ // If the path does not exist, we return early.
_, err := os.Stat(envValue)
if err != nil {
if os.IsNotExist(err) {
- log.Debugf(ctx, "%s at %s does not exist, ignoring %s", envVarName, envValue, versionVarName)
+ log.Debugf(ctx, "%s at %s does not exist", envVarName, envValue)
return "", nil
} else {
return "", err
}
}
+
+ // If the version environment variable is not set, we directly return the value of the environment variable.
+ if versionValue == "" {
+ return envValue, nil
+ }
+
+ // When the version environment variable is set, we check if it matches the current version.
+ // If it does not match, we return an empty string.
+ if versionValue != currentVersion {
+ log.Debugf(ctx, "%s as %s does not match the current version %s, ignoring %s", versionVarName, versionValue, currentVersion, envVarName)
+ return "", nil
+ }
return envValue, nil
}
diff --git a/bundle/deploy/terraform/init_test.go b/bundle/deploy/terraform/init_test.go
index ece897193a..ffc2158516 100644
--- a/bundle/deploy/terraform/init_test.go
+++ b/bundle/deploy/terraform/init_test.go
@@ -12,6 +12,7 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/internal/tf/schema"
+ "github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/env"
"github.com/hashicorp/hc-install/product"
"github.com/stretchr/testify/assert"
@@ -392,3 +393,60 @@ func createTempFile(t *testing.T, dest string, name string, executable bool) str
}
return binPath
}
+
+func TestGetEnvVarWithMatchingVersion(t *testing.T) {
+ envVarName := "FOO"
+ versionVarName := "FOO_VERSION"
+
+ tmp := t.TempDir()
+ testutil.Touch(t, tmp, "bar")
+
+ var tc = []struct {
+ envValue string
+ versionValue string
+ currentVersion string
+ expected string
+ }{
+ {
+ envValue: filepath.Join(tmp, "bar"),
+ versionValue: "1.2.3",
+ currentVersion: "1.2.3",
+ expected: filepath.Join(tmp, "bar"),
+ },
+ {
+ envValue: filepath.Join(tmp, "does-not-exist"),
+ versionValue: "1.2.3",
+ currentVersion: "1.2.3",
+ expected: "",
+ },
+ {
+ envValue: filepath.Join(tmp, "bar"),
+ versionValue: "1.2.3",
+ currentVersion: "1.2.4",
+ expected: "",
+ },
+ {
+ envValue: "",
+ versionValue: "1.2.3",
+ currentVersion: "1.2.3",
+ expected: "",
+ },
+ {
+ envValue: filepath.Join(tmp, "bar"),
+ versionValue: "",
+ currentVersion: "1.2.3",
+ expected: filepath.Join(tmp, "bar"),
+ },
+ }
+
+ for _, c := range tc {
+ t.Run("", func(t *testing.T) {
+ t.Setenv(envVarName, c.envValue)
+ t.Setenv(versionVarName, c.versionValue)
+
+ actual, err := getEnvVarWithMatchingVersion(context.Background(), envVarName, versionVarName, c.currentVersion)
+ require.NoError(t, err)
+ assert.Equal(t, c.expected, actual)
+ })
+ }
+}
diff --git a/docker/config.tfrc b/docker/config.tfrc
new file mode 100644
index 0000000000..123f6d6398
--- /dev/null
+++ b/docker/config.tfrc
@@ -0,0 +1,6 @@
+provider_installation {
+ filesystem_mirror {
+ path = "/app/providers"
+ include = ["registry.terraform.io/databricks/databricks"]
+ }
+}
diff --git a/docker/setup.sh b/docker/setup.sh
new file mode 100755
index 0000000000..3f6c09dc72
--- /dev/null
+++ b/docker/setup.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -euo pipefail
+
+DATABRICKS_TF_VERSION=$(/app/databricks bundle debug terraform --output json | jq -r .terraform.version)
+DATABRICKS_TF_PROVIDER_VERSION=$(/app/databricks bundle debug terraform --output json | jq -r .terraform.providerVersion)
+
+# Download the terraform binary
+mkdir -p zip
+wget https://releases.hashicorp.com/terraform/${DATABRICKS_TF_VERSION}/terraform_${DATABRICKS_TF_VERSION}_linux_${ARCH}.zip -O zip/terraform.zip
+unzip zip/terraform.zip -d zip/terraform
+mkdir -p /app/bin
+mv zip/terraform/terraform /app/bin/terraform
+
+# Download the provider plugin
+TF_PROVIDER_NAME=terraform-provider-databricks_${DATABRICKS_TF_PROVIDER_VERSION}_linux_${ARCH}.zip
+mkdir -p /app/providers/registry.terraform.io/databricks/databricks
+wget https://github.com/databricks/terraform-provider-databricks/releases/download/v${DATABRICKS_TF_PROVIDER_VERSION}/${TF_PROVIDER_NAME} -O /app/providers/registry.terraform.io/databricks/databricks/${TF_PROVIDER_NAME}