diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43ceea2c..f9b4ec15 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 0cf87a9c..e4406874 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 00000000..d4e7614c --- /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 9f423531..69ae70ba 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 ece89719..ffc21585 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 00000000..123f6d63 --- /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 00000000..3f6c09dc --- /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}