Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bundle debug terraform command #1294

Merged
merged 29 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3ca75d
Add `bundle dependencies` command
ilia-db Mar 18, 2024
e8ec958
Check versions and files/dirs
ilia-db Mar 19, 2024
aff7024
Update log message
ilia-db Mar 19, 2024
9135426
Merge branch 'main' into terraform-dependencies
ilia-db Mar 19, 2024
d99a003
Add tests for TF_PLUGIN_CACHE_DIR
ilia-db Mar 19, 2024
8a3b0b7
Add tests for DATABRICKS_TF_EXEC_PATH
ilia-db Mar 19, 2024
c157a5c
Improve tests
ilia-db Mar 19, 2024
f6142ce
Do not override existing execPath from the bundle
ilia-db Mar 20, 2024
91924ed
Improve tests
ilia-db Mar 20, 2024
55c7808
Close terraform binary file after creation
ilia-db Mar 20, 2024
1e549a4
Add debug logs, improve tests
ilia-db Mar 20, 2024
70334a5
Use TF_CLI_CONFIG_FILE instead of TF_PLUGIN_CACHE_DIR
ilia-db Mar 21, 2024
20ff8e1
Add plain text output
ilia-db Mar 21, 2024
58bbd71
Remove unnecessary test
ilia-db Mar 21, 2024
c5df7af
Formatting
ilia-db Mar 21, 2024
51175c9
Better template message
ilia-db Mar 21, 2024
7e42269
Merge branch 'main' into terraform-dependencies
ilia-db Mar 21, 2024
92e5cef
Update json names
ilia-db Mar 22, 2024
5f4ddb4
Update text output
ilia-db Mar 26, 2024
9e84685
Update filesystem mirror explaination
ilia-db Mar 26, 2024
e10464f
Move to `debug terraform` command
ilia-db Mar 26, 2024
cd31b66
Merge branch 'main' into terraform-dependencies
ilia-db Mar 26, 2024
d38d946
Add comment for getEnvVarWithMatchingVersion
ilia-db Mar 28, 2024
8884a41
Merge branch 'main' into terraform-dependencies
ilia-db Mar 28, 2024
d99642f
Update tests
ilia-db Mar 28, 2024
7413253
Update bundle/deploy/terraform/init.go
ilia-db Apr 2, 2024
138a9e1
Move env vars into consts
ilia-db Apr 2, 2024
d921a82
Fix typo
ilia-db Apr 2, 2024
d157a2c
Merge branch 'main' into terraform-dependencies
ilia-db Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions bundle/deploy/terraform/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/internal/tf/schema"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/log"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
"github.com/hashicorp/terraform-exec/tfexec"
Expand All @@ -40,6 +40,17 @@ func (m *initialize) findExecPath(ctx context.Context, b *bundle.Bundle, tf *con
return tf.ExecPath, nil
}

// Load exec path from the environment if it matches the currently used version.
envExecPath, err := getEnvVarWithMatchingVersion(ctx, TerraformExecPathEnv, TerraformVersionEnv, TerraformVersion.String())
if err != nil {
return "", err
}
if envExecPath != "" {
tf.ExecPath = envExecPath
log.Debugf(ctx, "Using Terraform from %s at %s", TerraformExecPathEnv, tf.ExecPath)
return tf.ExecPath, nil
}
ilia-db marked this conversation as resolved.
Show resolved Hide resolved

binDir, err := b.CacheDir(context.Background(), "bin")
if err != nil {
return "", err
Expand All @@ -60,7 +71,7 @@ func (m *initialize) findExecPath(ctx context.Context, b *bundle.Bundle, tf *con
// Download Terraform to private bin directory.
installer := &releases.ExactVersion{
Product: product.Terraform,
Version: version.Must(version.NewVersion("1.5.5")),
Version: TerraformVersion,
InstallDir: binDir,
Timeout: 1 * time.Minute,
}
Expand Down Expand Up @@ -98,14 +109,55 @@ func inheritEnvVars(ctx context.Context, environ map[string]string) error {
}

// Include $TF_CLI_CONFIG_FILE to override terraform provider in development.
configFile, ok := env.Lookup(ctx, "TF_CLI_CONFIG_FILE")
// See: https://developer.hashicorp.com/terraform/cli/config/config-file#explicit-installation-method-configuration
devConfigFile, ok := env.Lookup(ctx, "TF_CLI_CONFIG_FILE")
if ok {
environ["TF_CLI_CONFIG_FILE"] = devConfigFile
}

// Map $DATABRICKS_TF_CLI_CONFIG_FILE to $TF_CLI_CONFIG_FILE
// VSCode extension provides a file with the "provider_installation.filesystem_mirror" configuration.
// We only use it if the provider version matches the currently used version,
// otherwise terraform will fail to download the right version (even with unrestricted internet access).
configFile, err := getEnvVarWithMatchingVersion(ctx, TerraformCliConfigPathEnv, TerraformProviderVersionEnv, schema.ProviderVersion)
if err != nil {
return err
}
if configFile != "" {
log.Debugf(ctx, "Using Terraform CLI config from %s at %s", TerraformCliConfigPathEnv, configFile)
environ["TF_CLI_CONFIG_FILE"] = configFile
}

return nil
}

// Example: this function will return a value of TF_EXEC_PATH only if the path exists and if TF_VERSION matches the TerraformVersion.
// This function is used for env vars set by the Databricks VSCode extension. The variables are intended to be used by the CLI
// bundled with the Databricks VSCode extension, but users can use different CLI versions in the VSCode terminals, in which case we want to ignore
// the variables if that CLI uses different versions of the dependencies.
func getEnvVarWithMatchingVersion(ctx context.Context, envVarName string, versionVarName string, currentVersion string) (string, error) {
ilia-db marked this conversation as resolved.
Show resolved Hide resolved
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 "", nil
}
_, 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)
return "", nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a hard error? If someone sets these env vars, they intend to use them. Mistakes are ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment varialbes in the terminals can have a long timespan, and we can't change them after the initial setup.

One edge case is when users uninstall the extension. The variables will stick in the persistent terminal sessions utill users re-create terminals. Since we don't copy the dependencies and point env vars to the internals of the installed extensions, after the uninstall the variables will point to the place that no longer exist. We can copy dependencies to .databricks folder for each project, but I don't think it's worth the hassle

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this tie in with Databricks Terminals? If these are only set in those then we don't have to bother with long lifetimes of env vars in terminals.

Copy link
Contributor

@shreyas-goenka shreyas-goenka Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't change them after the initial setup

For my knowledge, updating the extensions requires reloading the IDE, and then reloading the IDE should refresh the env vars right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reloading VSCode doesn't restart terminals, they are persistent (at least by default), and so the env vars also persist.

We've not yet made a decision about "Databricks Terminal". We can change the logic here afterwards though

} else {
return "", err
}
}
return envValue, nil
}

// This function sets temp dir location for terraform to use. If user does not
// specify anything here, we fall back to a `tmp` directory in the bundle's cache
// directory
Expand Down
123 changes: 123 additions & 0 deletions bundle/deploy/terraform/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/internal/tf/schema"
"github.com/databricks/cli/libs/env"
"github.com/hashicorp/hc-install/product"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -269,3 +273,122 @@ func TestSetUserProfileFromInheritEnvVars(t *testing.T) {
assert.Contains(t, env, "USERPROFILE")
assert.Equal(t, env["USERPROFILE"], "c:\\foo\\c")
}

func TestInheritEnvVarsWithAbsentTFConfigFile(t *testing.T) {
ctx := context.Background()
envMap := map[string]string{}
ctx = env.Set(ctx, "DATABRICKS_TF_PROVIDER_VERSION", schema.ProviderVersion)
ctx = env.Set(ctx, "DATABRICKS_TF_CLI_CONFIG_FILE", "/tmp/config.tfrc")
err := inheritEnvVars(ctx, envMap)
require.NoError(t, err)
require.NotContains(t, envMap, "TF_CLI_CONFIG_FILE")
}

func TestInheritEnvVarsWithWrongTFProviderVersion(t *testing.T) {
ctx := context.Background()
envMap := map[string]string{}
configFile := createTempFile(t, t.TempDir(), "config.tfrc", false)
ctx = env.Set(ctx, "DATABRICKS_TF_PROVIDER_VERSION", "wrong")
ctx = env.Set(ctx, "DATABRICKS_TF_CLI_CONFIG_FILE", configFile)
err := inheritEnvVars(ctx, envMap)
require.NoError(t, err)
require.NotContains(t, envMap, "TF_CLI_CONFIG_FILE")
}

func TestInheritEnvVarsWithCorrectTFCLIConfigFile(t *testing.T) {
ctx := context.Background()
envMap := map[string]string{}
configFile := createTempFile(t, t.TempDir(), "config.tfrc", false)
ctx = env.Set(ctx, "DATABRICKS_TF_PROVIDER_VERSION", schema.ProviderVersion)
ctx = env.Set(ctx, "DATABRICKS_TF_CLI_CONFIG_FILE", configFile)
err := inheritEnvVars(ctx, envMap)
require.NoError(t, err)
require.Contains(t, envMap, "TF_CLI_CONFIG_FILE")
require.Equal(t, configFile, envMap["TF_CLI_CONFIG_FILE"])
}

func TestFindExecPathFromEnvironmentWithWrongVersion(t *testing.T) {
ctx := context.Background()
m := &initialize{}
b := &bundle.Bundle{
RootPath: t.TempDir(),
Config: config.Root{
Bundle: config.Bundle{
Target: "whatever",
Terraform: &config.Terraform{},
},
},
}
// Create a pre-existing terraform bin to avoid downloading it
cacheDir, _ := b.CacheDir(ctx, "bin")
existingExecPath := createTempFile(t, cacheDir, product.Terraform.BinaryName(), true)
// Create a new terraform binary and expose it through env vars
tmpBinPath := createTempFile(t, t.TempDir(), "terraform-bin", true)
ctx = env.Set(ctx, "DATABRICKS_TF_VERSION", "1.2.3")
ctx = env.Set(ctx, "DATABRICKS_TF_EXEC_PATH", tmpBinPath)
_, err := m.findExecPath(ctx, b, b.Config.Bundle.Terraform)
require.NoError(t, err)
require.Equal(t, existingExecPath, b.Config.Bundle.Terraform.ExecPath)
}

func TestFindExecPathFromEnvironmentWithCorrectVersionAndNoBinary(t *testing.T) {
ctx := context.Background()
m := &initialize{}
b := &bundle.Bundle{
RootPath: t.TempDir(),
Config: config.Root{
Bundle: config.Bundle{
Target: "whatever",
Terraform: &config.Terraform{},
},
},
}
// Create a pre-existing terraform bin to avoid downloading it
cacheDir, _ := b.CacheDir(ctx, "bin")
existingExecPath := createTempFile(t, cacheDir, product.Terraform.BinaryName(), true)

ctx = env.Set(ctx, "DATABRICKS_TF_VERSION", TerraformVersion.String())
ctx = env.Set(ctx, "DATABRICKS_TF_EXEC_PATH", "/tmp/terraform")
_, err := m.findExecPath(ctx, b, b.Config.Bundle.Terraform)
require.NoError(t, err)
require.Equal(t, existingExecPath, b.Config.Bundle.Terraform.ExecPath)
}

func TestFindExecPathFromEnvironmentWithCorrectVersionAndBinary(t *testing.T) {
ctx := context.Background()
m := &initialize{}
b := &bundle.Bundle{
RootPath: t.TempDir(),
Config: config.Root{
Bundle: config.Bundle{
Target: "whatever",
Terraform: &config.Terraform{},
},
},
}
// Create a pre-existing terraform bin to avoid downloading it
cacheDir, _ := b.CacheDir(ctx, "bin")
createTempFile(t, cacheDir, product.Terraform.BinaryName(), true)
// Create a new terraform binary and expose it through env vars
tmpBinPath := createTempFile(t, t.TempDir(), "terraform-bin", true)
ctx = env.Set(ctx, "DATABRICKS_TF_VERSION", TerraformVersion.String())
ctx = env.Set(ctx, "DATABRICKS_TF_EXEC_PATH", tmpBinPath)
_, err := m.findExecPath(ctx, b, b.Config.Bundle.Terraform)
require.NoError(t, err)
require.Equal(t, tmpBinPath, b.Config.Bundle.Terraform.ExecPath)
}

func createTempFile(t *testing.T, dest string, name string, executable bool) string {
binPath := filepath.Join(dest, name)
f, err := os.Create(binPath)
require.NoError(t, err)
defer func() {
err = f.Close()
require.NoError(t, err)
}()
if executable {
err = f.Chmod(0777)
require.NoError(t, err)
}
return binPath
}
30 changes: 30 additions & 0 deletions bundle/deploy/terraform/pkg.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
package terraform

import (
"github.com/databricks/cli/bundle/internal/tf/schema"
"github.com/hashicorp/go-version"
)

const TerraformStateFileName = "terraform.tfstate"
const TerraformConfigFileName = "bundle.tf.json"

// Users can provide their own terraform binary and databricks terraform provider by setting the following environment variables.
// This allows users to use the CLI in an air-gapped environments. See the `debug terraform` command.
const TerraformExecPathEnv = "DATABRICKS_TF_EXEC_PATH"
const TerraformVersionEnv = "DATABRICKS_TF_VERSION"
const TerraformCliConfigPathEnv = "DATABRICKS_TF_CLI_CONFIG_FILE"
const TerraformProviderVersionEnv = "DATABRICKS_TF_PROVIDER_VERSION"

var TerraformVersion = version.Must(version.NewVersion("1.5.5"))

type TerraformMetadata struct {
Version string `json:"version"`
ProviderHost string `json:"providerHost"`
ProviderSource string `json:"providerSource"`
ProviderVersion string `json:"providerVersion"`
}

func NewTerraformMetadata() *TerraformMetadata {
return &TerraformMetadata{
Version: TerraformVersion.String(),
ProviderHost: schema.ProviderHost,
ProviderSource: schema.ProviderSource,
ProviderVersion: schema.ProviderVersion,
}
}
8 changes: 6 additions & 2 deletions bundle/internal/tf/codegen/templates/root.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ type Root struct {
Resource *Resources `json:"resource,omitempty"`
}

const ProviderHost = "registry.terraform.io"
const ProviderSource = "databricks/databricks"
const ProviderVersion = "{{ .ProviderVersion }}"

func NewRoot() *Root {
return &Root{
Terraform: map[string]interface{}{
"required_providers": map[string]interface{}{
"databricks": map[string]interface{}{
"source": "databricks/databricks",
"version": "{{ .ProviderVersion }}",
"source": ProviderSource,
"version": ProviderVersion,
},
},
},
Expand Down
8 changes: 6 additions & 2 deletions bundle/internal/tf/schema/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ type Root struct {
Resource *Resources `json:"resource,omitempty"`
}

const ProviderHost = "registry.terraform.io"
ilia-db marked this conversation as resolved.
Show resolved Hide resolved
const ProviderSource = "databricks/databricks"
const ProviderVersion = "1.38.0"

func NewRoot() *Root {
return &Root{
Terraform: map[string]interface{}{
"required_providers": map[string]interface{}{
"databricks": map[string]interface{}{
"source": "databricks/databricks",
"version": "1.38.0",
"source": ProviderSource,
"version": ProviderVersion,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions cmd/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func New() *cobra.Command {
cmd.AddCommand(newInitCommand())
cmd.AddCommand(newSummaryCommand())
cmd.AddCommand(newGenerateCommand())
cmd.AddCommand(newDebugCommand())
cmd.AddCommand(deployment.NewDeploymentCommand())
return cmd
}
18 changes: 18 additions & 0 deletions cmd/bundle/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package bundle

import (
"github.com/databricks/cli/cmd/bundle/debug"
"github.com/spf13/cobra"
)

func newDebugCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "debug",
Short: "Debug information about bundles",
Long: "Debug information about bundles",
// This command group is currently intended for the Databricks VSCode extension only
Hidden: true,
}
cmd.AddCommand(debug.NewTerraformCommand())
return cmd
}
Loading
Loading