Make bundle loaders return diagnostics (#1319)

## Changes

The function signature of Cobra's `PreRunE` function has an `error`
return value. We'd like to start returning `diag.Diagnostics` after
loading a bundle, so this is incompatible. This change modifies all
usage of `PreRunE` to load a bundle to inline function calls in the
command's `RunE` function.

## Tests

* Unit tests pass.
* Integration tests pass.
This commit is contained in:
Pieter Noordhuis 2024-03-28 11:32:34 +01:00 committed by GitHub
parent 5df4c7e134
commit b21e3c81cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 305 additions and 199 deletions

View File

@ -16,7 +16,6 @@ func newDeployCommand() *cobra.Command {
Use: "deploy", Use: "deploy",
Short: "Deploy bundle", Short: "Deploy bundle",
Args: root.NoArgs, Args: root.NoArgs,
PreRunE: utils.ConfigureBundleWithVariables,
} }
var force bool var force bool
@ -30,7 +29,10 @@ func newDeployCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
bundle.ApplyFunc(ctx, b, func(context.Context, *bundle.Bundle) diag.Diagnostics { bundle.ApplyFunc(ctx, b, func(context.Context, *bundle.Bundle) diag.Diagnostics {
b.Config.Bundle.Force = force b.Config.Bundle.Force = force
@ -46,7 +48,7 @@ func newDeployCommand() *cobra.Command {
return nil return nil
}) })
diags := bundle.Apply(ctx, b, bundle.Seq( diags = bundle.Apply(ctx, b, bundle.Seq(
phases.Initialize(), phases.Initialize(),
phases.Build(), phases.Build(),
phases.Deploy(), phases.Deploy(),

View File

@ -19,7 +19,6 @@ func newBindCommand() *cobra.Command {
Use: "bind KEY RESOURCE_ID", Use: "bind KEY RESOURCE_ID",
Short: "Bind bundle-defined resources to existing resources", Short: "Bind bundle-defined resources to existing resources",
Args: root.ExactArgs(2), Args: root.ExactArgs(2),
PreRunE: utils.ConfigureBundleWithVariables,
} }
var autoApprove bool var autoApprove bool
@ -29,7 +28,11 @@ func newBindCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
resource, err := b.Config.Resources.FindResourceByConfigKey(args[0]) resource, err := b.Config.Resources.FindResourceByConfigKey(args[0])
if err != nil { if err != nil {
return err return err
@ -50,7 +53,7 @@ func newBindCommand() *cobra.Command {
return nil return nil
}) })
diags := bundle.Apply(ctx, b, bundle.Seq( diags = bundle.Apply(ctx, b, bundle.Seq(
phases.Initialize(), phases.Initialize(),
phases.Bind(&terraform.BindOptions{ phases.Bind(&terraform.BindOptions{
AutoApprove: autoApprove, AutoApprove: autoApprove,

View File

@ -16,7 +16,6 @@ func newUnbindCommand() *cobra.Command {
Use: "unbind KEY", Use: "unbind KEY",
Short: "Unbind bundle-defined resources from its managed remote resource", Short: "Unbind bundle-defined resources from its managed remote resource",
Args: root.ExactArgs(1), Args: root.ExactArgs(1),
PreRunE: utils.ConfigureBundleWithVariables,
} }
var forceLock bool var forceLock bool
@ -24,7 +23,11 @@ func newUnbindCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
resource, err := b.Config.Resources.FindResourceByConfigKey(args[0]) resource, err := b.Config.Resources.FindResourceByConfigKey(args[0])
if err != nil { if err != nil {
return err return err
@ -35,7 +38,7 @@ func newUnbindCommand() *cobra.Command {
return nil return nil
}) })
diags := bundle.Apply(cmd.Context(), b, bundle.Seq( diags = bundle.Apply(cmd.Context(), b, bundle.Seq(
phases.Initialize(), phases.Initialize(),
phases.Unbind(resource.TerraformResourceName(), args[0]), phases.Unbind(resource.TerraformResourceName(), args[0]),
)) ))

View File

@ -21,7 +21,6 @@ func newDestroyCommand() *cobra.Command {
Use: "destroy", Use: "destroy",
Short: "Destroy deployed bundle resources", Short: "Destroy deployed bundle resources",
Args: root.NoArgs, Args: root.NoArgs,
PreRunE: utils.ConfigureBundleWithVariables,
} }
var autoApprove bool var autoApprove bool
@ -31,7 +30,10 @@ func newDestroyCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// If `--force-lock` is specified, force acquisition of the deployment lock. // If `--force-lock` is specified, force acquisition of the deployment lock.
@ -58,7 +60,7 @@ func newDestroyCommand() *cobra.Command {
return fmt.Errorf("please specify --auto-approve since selected logging format is json") return fmt.Errorf("please specify --auto-approve since selected logging format is json")
} }
diags := bundle.Apply(ctx, b, bundle.Seq( diags = bundle.Apply(ctx, b, bundle.Seq(
phases.Initialize(), phases.Initialize(),
phases.Build(), phases.Build(),
phases.Destroy(), phases.Destroy(),

View File

@ -2,7 +2,6 @@ package bundle
import ( import (
"github.com/databricks/cli/cmd/bundle/generate" "github.com/databricks/cli/cmd/bundle/generate"
"github.com/databricks/cli/cmd/bundle/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -13,7 +12,6 @@ func newGenerateCommand() *cobra.Command {
Use: "generate", Use: "generate",
Short: "Generate bundle configuration", Short: "Generate bundle configuration",
Long: "Generate bundle configuration", Long: "Generate bundle configuration",
PreRunE: utils.ConfigureBundleWithVariables,
} }
cmd.AddCommand(generate.NewGenerateJobCommand()) cmd.AddCommand(generate.NewGenerateJobCommand())

View File

@ -5,7 +5,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/generate" "github.com/databricks/cli/bundle/config/generate"
"github.com/databricks/cli/cmd/root" "github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/cmdio"
@ -26,7 +25,6 @@ func NewGenerateJobCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "job", Use: "job",
Short: "Generate bundle configuration for a job", Short: "Generate bundle configuration for a job",
PreRunE: root.MustConfigureBundle,
} }
cmd.Flags().Int64Var(&jobId, "existing-job-id", 0, `Job ID of the job to generate config for`) cmd.Flags().Int64Var(&jobId, "existing-job-id", 0, `Job ID of the job to generate config for`)
@ -43,9 +41,12 @@ func NewGenerateJobCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := root.MustConfigureBundle(cmd)
w := b.WorkspaceClient() if err := diags.Error(); err != nil {
return diags.Error()
}
w := b.WorkspaceClient()
job, err := w.Jobs.Get(ctx, jobs.GetJobRequest{JobId: jobId}) job, err := w.Jobs.Get(ctx, jobs.GetJobRequest{JobId: jobId})
if err != nil { if err != nil {
return err return err

View File

@ -5,7 +5,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/generate" "github.com/databricks/cli/bundle/config/generate"
"github.com/databricks/cli/cmd/root" "github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/cmdio"
@ -26,7 +25,6 @@ func NewGeneratePipelineCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "pipeline", Use: "pipeline",
Short: "Generate bundle configuration for a pipeline", Short: "Generate bundle configuration for a pipeline",
PreRunE: root.MustConfigureBundle,
} }
cmd.Flags().StringVar(&pipelineId, "existing-pipeline-id", "", `ID of the pipeline to generate config for`) cmd.Flags().StringVar(&pipelineId, "existing-pipeline-id", "", `ID of the pipeline to generate config for`)
@ -43,9 +41,12 @@ func NewGeneratePipelineCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := root.MustConfigureBundle(cmd)
w := b.WorkspaceClient() if err := diags.Error(); err != nil {
return diags.Error()
}
w := b.WorkspaceClient()
pipeline, err := w.Pipelines.Get(ctx, pipelines.GetPipelineRequest{PipelineId: pipelineId}) pipeline, err := w.Pipelines.Get(ctx, pipelines.GetPipelineRequest{PipelineId: pipelineId})
if err != nil { if err != nil {
return err return err

View File

@ -16,8 +16,6 @@ func newLaunchCommand() *cobra.Command {
// We're not ready to expose this command until we specify its semantics. // We're not ready to expose this command until we specify its semantics.
Hidden: true, Hidden: true,
PreRunE: root.MustConfigureBundle,
} }
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {

View File

@ -20,7 +20,6 @@ func newRunCommand() *cobra.Command {
Use: "run [flags] KEY", Use: "run [flags] KEY",
Short: "Run a resource (e.g. a job or a pipeline)", Short: "Run a resource (e.g. a job or a pipeline)",
Args: root.MaximumNArgs(1), Args: root.MaximumNArgs(1),
PreRunE: utils.ConfigureBundleWithVariables,
} }
var runOptions run.Options var runOptions run.Options
@ -33,9 +32,12 @@ func newRunCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
b := bundle.Get(ctx) b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
diags := bundle.Apply(ctx, b, bundle.Seq( diags = bundle.Apply(ctx, b, bundle.Seq(
phases.Initialize(), phases.Initialize(),
terraform.Interpolate(), terraform.Interpolate(),
terraform.Write(), terraform.Write(),
@ -109,15 +111,14 @@ func newRunCommand() *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
err := root.MustConfigureBundle(cmd, args) b, diags := root.MustConfigureBundle(cmd)
if err != nil { if err := diags.Error(); err != nil {
cobra.CompErrorln(err.Error()) cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
// No completion in the context of a bundle. // No completion in the context of a bundle.
// Source and destination paths are taken from bundle configuration. // Source and destination paths are taken from bundle configuration.
b := bundle.GetOrNil(cmd.Context())
if b == nil { if b == nil {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }

View File

@ -21,7 +21,6 @@ func newSummaryCommand() *cobra.Command {
Use: "summary", Use: "summary",
Short: "Describe the bundle resources and their deployment states", Short: "Describe the bundle resources and their deployment states",
Args: root.NoArgs, Args: root.NoArgs,
PreRunE: utils.ConfigureBundleWithVariables,
// This command is currently intended for the Databricks VSCode extension only // This command is currently intended for the Databricks VSCode extension only
Hidden: true, Hidden: true,
@ -31,14 +30,18 @@ func newSummaryCommand() *cobra.Command {
cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace") cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace")
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
b := bundle.Get(cmd.Context()) ctx := cmd.Context()
b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
diags := bundle.Apply(cmd.Context(), b, phases.Initialize()) diags = bundle.Apply(ctx, b, phases.Initialize())
if err := diags.Error(); err != nil { if err := diags.Error(); err != nil {
return err return err
} }
cacheDir, err := terraform.Dir(cmd.Context(), b) cacheDir, err := terraform.Dir(ctx, b)
if err != nil { if err != nil {
return err return err
} }
@ -47,7 +50,7 @@ func newSummaryCommand() *cobra.Command {
noCache := errors.Is(stateFileErr, os.ErrNotExist) || errors.Is(configFileErr, os.ErrNotExist) noCache := errors.Is(stateFileErr, os.ErrNotExist) || errors.Is(configFileErr, os.ErrNotExist)
if forcePull || noCache { if forcePull || noCache {
diags = bundle.Apply(cmd.Context(), b, bundle.Seq( diags = bundle.Apply(ctx, b, bundle.Seq(
terraform.StatePull(), terraform.StatePull(),
terraform.Interpolate(), terraform.Interpolate(),
terraform.Write(), terraform.Write(),
@ -57,7 +60,7 @@ func newSummaryCommand() *cobra.Command {
} }
} }
diags = bundle.Apply(cmd.Context(), b, terraform.Load()) diags = bundle.Apply(ctx, b, terraform.Load())
if err := diags.Error(); err != nil { if err := diags.Error(); err != nil {
return err return err
} }

View File

@ -36,8 +36,6 @@ func newSyncCommand() *cobra.Command {
Use: "sync [flags]", Use: "sync [flags]",
Short: "Synchronize bundle tree to the workspace", Short: "Synchronize bundle tree to the workspace",
Args: root.NoArgs, Args: root.NoArgs,
PreRunE: utils.ConfigureBundleWithVariables,
} }
var f syncFlags var f syncFlags
@ -46,10 +44,14 @@ func newSyncCommand() *cobra.Command {
cmd.Flags().BoolVar(&f.watch, "watch", false, "watch local file system for changes") cmd.Flags().BoolVar(&f.watch, "watch", false, "watch local file system for changes")
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
b := bundle.Get(cmd.Context()) ctx := cmd.Context()
b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
// Run initialize phase to make sure paths are set. // Run initialize phase to make sure paths are set.
diags := bundle.Apply(cmd.Context(), b, phases.Initialize()) diags = bundle.Apply(ctx, b, phases.Initialize())
if err := diags.Error(); err != nil { if err := diags.Error(); err != nil {
return err return err
} }
@ -59,7 +61,6 @@ func newSyncCommand() *cobra.Command {
return err return err
} }
ctx := cmd.Context()
s, err := sync.New(ctx, *opts) s, err := sync.New(ctx, *opts)
if err != nil { if err != nil {
return err return err

View File

@ -3,7 +3,6 @@ package bundle
import ( import (
"fmt" "fmt"
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,8 +14,6 @@ func newTestCommand() *cobra.Command {
// We're not ready to expose this command until we specify its semantics. // We're not ready to expose this command until we specify its semantics.
Hidden: true, Hidden: true,
PreRunE: root.MustConfigureBundle,
} }
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {

View File

@ -9,23 +9,30 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func ConfigureBundleWithVariables(cmd *cobra.Command, args []string) error { func configureVariables(cmd *cobra.Command, b *bundle.Bundle, variables []string) diag.Diagnostics {
return bundle.ApplyFunc(cmd.Context(), b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
err := b.Config.InitializeVariables(variables)
return diag.FromErr(err)
})
}
func ConfigureBundleWithVariables(cmd *cobra.Command) (*bundle.Bundle, diag.Diagnostics) {
// Load bundle config and apply target // Load bundle config and apply target
err := root.MustConfigureBundle(cmd, args) b, diags := root.MustConfigureBundle(cmd)
if err != nil { if diags.HasError() {
return err return nil, diags
} }
variables, err := cmd.Flags().GetStringSlice("var") variables, err := cmd.Flags().GetStringSlice("var")
if err != nil { if err != nil {
return err return nil, diag.FromErr(err)
} }
// Initialize variables by assigning them values passed as command line flags // Initialize variables by assigning them values passed as command line flags
b := bundle.Get(cmd.Context()) diags = diags.Extend(configureVariables(cmd, b, variables))
diags := bundle.ApplyFunc(cmd.Context(), b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { if diags.HasError() {
err := b.Config.InitializeVariables(variables) return nil, diags
return diag.FromErr(err) }
})
return diags.Error() return b, diags
} }

View File

@ -16,13 +16,16 @@ func newValidateCommand() *cobra.Command {
Use: "validate", Use: "validate",
Short: "Validate configuration", Short: "Validate configuration",
Args: root.NoArgs, Args: root.NoArgs,
PreRunE: utils.ConfigureBundleWithVariables,
} }
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
b := bundle.Get(cmd.Context()) ctx := cmd.Context()
b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}
diags := bundle.Apply(cmd.Context(), b, phases.Initialize()) diags = bundle.Apply(ctx, b, phases.Initialize())
if err := diags.Error(); err != nil { if err := diags.Error(); err != nil {
return err return err
} }

View File

@ -10,7 +10,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/cmd/root" "github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/internal/build" "github.com/databricks/cli/internal/build"
"github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/cmdio"
@ -203,11 +202,11 @@ func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.C
return lc, cfg, nil return lc, cfg, nil
} }
if e.IsBundleAware { if e.IsBundleAware {
err = root.TryConfigureBundle(cmd, []string{}) b, diags := root.TryConfigureBundle(cmd)
if err != nil { if err := diags.Error(); err != nil {
return nil, nil, fmt.Errorf("bundle: %w", err) return nil, nil, fmt.Errorf("bundle: %w", err)
} }
if b := bundle.GetOrNil(cmd.Context()); b != nil { if b != nil {
log.Infof(ctx, "Using login configuration from Databricks Asset Bundle") log.Infof(ctx, "Using login configuration from Databricks Asset Bundle")
return &loginConfig{}, b.WorkspaceClient().Config, nil return &loginConfig{}, b.WorkspaceClient().Config, nil
} }

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/databrickscfg" "github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go"
@ -149,11 +148,11 @@ func MustWorkspaceClient(cmd *cobra.Command, args []string) error {
// Try to load a bundle configuration if we're allowed to by the caller (see `./auth_options.go`). // Try to load a bundle configuration if we're allowed to by the caller (see `./auth_options.go`).
if !shouldSkipLoadBundle(cmd.Context()) { if !shouldSkipLoadBundle(cmd.Context()) {
err := TryConfigureBundle(cmd, args) b, diags := TryConfigureBundle(cmd)
if err != nil { if err := diags.Error(); err != nil {
return err return err
} }
if b := bundle.GetOrNil(cmd.Context()); b != nil { if b != nil {
client, err := b.InitializeWorkspaceClient() client, err := b.InitializeWorkspaceClient()
if err != nil { if err != nil {
return err return err

View File

@ -4,8 +4,8 @@ import (
"context" "context"
"github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/mutator"
"github.com/databricks/cli/bundle/env" "github.com/databricks/cli/bundle/env"
"github.com/databricks/cli/bundle/phases"
"github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/diag"
envlib "github.com/databricks/cli/libs/env" envlib "github.com/databricks/cli/libs/env"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -50,87 +50,100 @@ func getProfile(cmd *cobra.Command) (value string) {
return envlib.Get(cmd.Context(), "DATABRICKS_CONFIG_PROFILE") return envlib.Get(cmd.Context(), "DATABRICKS_CONFIG_PROFILE")
} }
// loadBundle loads the bundle configuration and applies default mutators. // configureProfile applies the profile flag to the bundle.
func loadBundle(cmd *cobra.Command, args []string, load func(ctx context.Context) (*bundle.Bundle, error)) (*bundle.Bundle, error) { func configureProfile(cmd *cobra.Command, b *bundle.Bundle) diag.Diagnostics {
ctx := cmd.Context()
b, err := load(ctx)
if err != nil {
return nil, err
}
// No bundle is fine in case of `TryConfigureBundle`.
if b == nil {
return nil, nil
}
profile := getProfile(cmd) profile := getProfile(cmd)
if profile != "" { if profile == "" {
diags := bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { return nil
}
return bundle.ApplyFunc(cmd.Context(), b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
b.Config.Workspace.Profile = profile b.Config.Workspace.Profile = profile
return nil return nil
}) })
if err := diags.Error(); err != nil {
return nil, err
}
}
diags := bundle.Apply(ctx, b, bundle.Seq(mutator.DefaultMutators()...))
if err := diags.Error(); err != nil {
return nil, err
}
return b, nil
}
// configureBundle loads the bundle configuration and configures it on the command's context.
func configureBundle(cmd *cobra.Command, args []string, load func(ctx context.Context) (*bundle.Bundle, error)) error {
b, err := loadBundle(cmd, args, load)
if err != nil {
return err
}
// No bundle is fine in case of `TryConfigureBundle`.
if b == nil {
return nil
} }
// configureBundle loads the bundle configuration and configures flag values, if any.
func configureBundle(cmd *cobra.Command, b *bundle.Bundle) (*bundle.Bundle, diag.Diagnostics) {
var m bundle.Mutator var m bundle.Mutator
env := getTarget(cmd) if target := getTarget(cmd); target == "" {
if env == "" { m = phases.LoadDefaultTarget()
m = mutator.SelectDefaultTarget()
} else { } else {
m = mutator.SelectTarget(env) m = phases.LoadNamedTarget(target)
} }
// Load bundle and select target.
ctx := cmd.Context() ctx := cmd.Context()
diags := bundle.Apply(ctx, b, m) diags := bundle.Apply(ctx, b, m)
if err := diags.Error(); err != nil { if diags.HasError() {
return err return nil, diags
} }
cmd.SetContext(bundle.Context(ctx, b)) // Configure the workspace profile if the flag has been set.
return nil diags = diags.Extend(configureProfile(cmd, b))
if diags.HasError() {
return nil, diags
}
return b, diags
} }
// MustConfigureBundle configures a bundle on the command context. // MustConfigureBundle configures a bundle on the command context.
func MustConfigureBundle(cmd *cobra.Command, args []string) error { func MustConfigureBundle(cmd *cobra.Command) (*bundle.Bundle, diag.Diagnostics) {
return configureBundle(cmd, args, bundle.MustLoad) // A bundle may be configured on the context when testing.
// If it is, return it immediately.
b := bundle.GetOrNil(cmd.Context())
if b != nil {
return b, nil
}
b, err := bundle.MustLoad(cmd.Context())
if err != nil {
return nil, diag.FromErr(err)
}
return configureBundle(cmd, b)
} }
// TryConfigureBundle configures a bundle on the command context // TryConfigureBundle configures a bundle on the command context
// if there is one, but doesn't fail if there isn't one. // if there is one, but doesn't fail if there isn't one.
func TryConfigureBundle(cmd *cobra.Command, args []string) error { func TryConfigureBundle(cmd *cobra.Command) (*bundle.Bundle, diag.Diagnostics) {
return configureBundle(cmd, args, bundle.TryLoad) // A bundle may be configured on the context when testing.
// If it is, return it immediately.
b := bundle.GetOrNil(cmd.Context())
if b != nil {
return b, nil
}
b, err := bundle.TryLoad(cmd.Context())
if err != nil {
return nil, diag.FromErr(err)
}
// No bundle is fine in this case.
if b == nil {
return nil, nil
}
return configureBundle(cmd, b)
} }
// targetCompletion executes to autocomplete the argument to the target flag. // targetCompletion executes to autocomplete the argument to the target flag.
func targetCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func targetCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
b, err := loadBundle(cmd, args, bundle.MustLoad) ctx := cmd.Context()
b, err := bundle.MustLoad(ctx)
if err != nil { if err != nil {
cobra.CompErrorln(err.Error()) cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
// Load bundle but don't select a target (we're completing those).
diags := bundle.Apply(ctx, b, phases.Load())
if err := diags.Error(); err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveError
}
return maps.Keys(b.Config.Targets), cobra.ShellCompDirectiveDefault return maps.Keys(b.Config.Targets), cobra.ShellCompDirectiveDefault
} }

View File

@ -2,16 +2,17 @@ package root
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"testing" "testing"
"github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/internal/testutil"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func setupDatabricksCfg(t *testing.T) { func setupDatabricksCfg(t *testing.T) {
@ -37,47 +38,61 @@ func emptyCommand(t *testing.T) *cobra.Command {
return cmd return cmd
} }
func setup(t *testing.T, cmd *cobra.Command, host string) *bundle.Bundle { func setupWithHost(t *testing.T, cmd *cobra.Command, host string) *bundle.Bundle {
setupDatabricksCfg(t) setupDatabricksCfg(t)
rootPath := t.TempDir() rootPath := t.TempDir()
testutil.Touch(t, rootPath, "databricks.yml") testutil.Chdir(t, rootPath)
err := configureBundle(cmd, []string{"validate"}, func(_ context.Context) (*bundle.Bundle, error) { contents := fmt.Sprintf(`
return &bundle.Bundle{ workspace:
RootPath: rootPath, host: %q
Config: config.Root{ `, host)
Bundle: config.Bundle{ err := os.WriteFile(filepath.Join(rootPath, "databricks.yml"), []byte(contents), 0644)
Name: "test", require.NoError(t, err)
},
Workspace: config.Workspace{ b, diags := MustConfigureBundle(cmd)
Host: host, require.NoError(t, diags.Error())
}, return b
}, }
}, nil
}) func setupWithProfile(t *testing.T, cmd *cobra.Command, profile string) *bundle.Bundle {
assert.NoError(t, err) setupDatabricksCfg(t)
return bundle.Get(cmd.Context())
rootPath := t.TempDir()
testutil.Chdir(t, rootPath)
contents := fmt.Sprintf(`
workspace:
profile: %q
`, profile)
err := os.WriteFile(filepath.Join(rootPath, "databricks.yml"), []byte(contents), 0644)
require.NoError(t, err)
b, diags := MustConfigureBundle(cmd)
require.NoError(t, diags.Error())
return b
} }
func TestBundleConfigureDefault(t *testing.T) { func TestBundleConfigureDefault(t *testing.T) {
testutil.CleanupEnvironment(t) testutil.CleanupEnvironment(t)
cmd := emptyCommand(t) cmd := emptyCommand(t)
b := setup(t, cmd, "https://x.com") b := setupWithHost(t, cmd, "https://x.com")
assert.NotPanics(t, func() {
b.WorkspaceClient() client, err := b.InitializeWorkspaceClient()
}) require.NoError(t, err)
assert.Equal(t, "https://x.com", client.Config.Host)
} }
func TestBundleConfigureWithMultipleMatches(t *testing.T) { func TestBundleConfigureWithMultipleMatches(t *testing.T) {
testutil.CleanupEnvironment(t) testutil.CleanupEnvironment(t)
cmd := emptyCommand(t) cmd := emptyCommand(t)
b := setup(t, cmd, "https://a.com") b := setupWithHost(t, cmd, "https://a.com")
assert.Panics(t, func() {
b.WorkspaceClient() _, err := b.InitializeWorkspaceClient()
}) assert.ErrorContains(t, err, "multiple profiles matched: PROFILE-1, PROFILE-2")
} }
func TestBundleConfigureWithNonExistentProfileFlag(t *testing.T) { func TestBundleConfigureWithNonExistentProfileFlag(t *testing.T) {
@ -85,11 +100,10 @@ func TestBundleConfigureWithNonExistentProfileFlag(t *testing.T) {
cmd := emptyCommand(t) cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("NOEXIST") cmd.Flag("profile").Value.Set("NOEXIST")
b := setupWithHost(t, cmd, "https://x.com")
b := setup(t, cmd, "https://x.com") _, err := b.InitializeWorkspaceClient()
assert.Panics(t, func() { assert.ErrorContains(t, err, "has no NOEXIST profile configured")
b.WorkspaceClient()
})
} }
func TestBundleConfigureWithMismatchedProfile(t *testing.T) { func TestBundleConfigureWithMismatchedProfile(t *testing.T) {
@ -97,11 +111,10 @@ func TestBundleConfigureWithMismatchedProfile(t *testing.T) {
cmd := emptyCommand(t) cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("PROFILE-1") cmd.Flag("profile").Value.Set("PROFILE-1")
b := setupWithHost(t, cmd, "https://x.com")
b := setup(t, cmd, "https://x.com") _, err := b.InitializeWorkspaceClient()
assert.PanicsWithError(t, "cannot resolve bundle auth configuration: config host mismatch: profile uses host https://a.com, but CLI configured to use https://x.com", func() { assert.ErrorContains(t, err, "config host mismatch: profile uses host https://a.com, but CLI configured to use https://x.com")
b.WorkspaceClient()
})
} }
func TestBundleConfigureWithCorrectProfile(t *testing.T) { func TestBundleConfigureWithCorrectProfile(t *testing.T) {
@ -109,35 +122,97 @@ func TestBundleConfigureWithCorrectProfile(t *testing.T) {
cmd := emptyCommand(t) cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("PROFILE-1") cmd.Flag("profile").Value.Set("PROFILE-1")
b := setupWithHost(t, cmd, "https://a.com")
b := setup(t, cmd, "https://a.com") client, err := b.InitializeWorkspaceClient()
assert.NotPanics(t, func() { require.NoError(t, err)
b.WorkspaceClient() assert.Equal(t, "https://a.com", client.Config.Host)
}) assert.Equal(t, "PROFILE-1", client.Config.Profile)
} }
func TestBundleConfigureWithMismatchedProfileEnvVariable(t *testing.T) { func TestBundleConfigureWithMismatchedProfileEnvVariable(t *testing.T) {
testutil.CleanupEnvironment(t) testutil.CleanupEnvironment(t)
t.Setenv("DATABRICKS_CONFIG_PROFILE", "PROFILE-1")
t.Setenv("DATABRICKS_CONFIG_PROFILE", "PROFILE-1")
cmd := emptyCommand(t) cmd := emptyCommand(t)
b := setup(t, cmd, "https://x.com") b := setupWithHost(t, cmd, "https://x.com")
assert.PanicsWithError(t, "cannot resolve bundle auth configuration: config host mismatch: profile uses host https://a.com, but CLI configured to use https://x.com", func() {
b.WorkspaceClient() _, err := b.InitializeWorkspaceClient()
}) assert.ErrorContains(t, err, "config host mismatch: profile uses host https://a.com, but CLI configured to use https://x.com")
} }
func TestBundleConfigureWithProfileFlagAndEnvVariable(t *testing.T) { func TestBundleConfigureWithProfileFlagAndEnvVariable(t *testing.T) {
testutil.CleanupEnvironment(t) testutil.CleanupEnvironment(t)
t.Setenv("DATABRICKS_CONFIG_PROFILE", "NOEXIST")
t.Setenv("DATABRICKS_CONFIG_PROFILE", "NOEXIST")
cmd := emptyCommand(t) cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("PROFILE-1") cmd.Flag("profile").Value.Set("PROFILE-1")
b := setupWithHost(t, cmd, "https://a.com")
b := setup(t, cmd, "https://a.com") client, err := b.InitializeWorkspaceClient()
assert.NotPanics(t, func() { require.NoError(t, err)
b.WorkspaceClient() assert.Equal(t, "https://a.com", client.Config.Host)
}) assert.Equal(t, "PROFILE-1", client.Config.Profile)
}
func TestBundleConfigureProfileDefault(t *testing.T) {
testutil.CleanupEnvironment(t)
// The profile in the databricks.yml file is used
cmd := emptyCommand(t)
b := setupWithProfile(t, cmd, "PROFILE-1")
client, err := b.InitializeWorkspaceClient()
require.NoError(t, err)
assert.Equal(t, "https://a.com", client.Config.Host)
assert.Equal(t, "a", client.Config.Token)
assert.Equal(t, "PROFILE-1", client.Config.Profile)
}
func TestBundleConfigureProfileFlag(t *testing.T) {
testutil.CleanupEnvironment(t)
// The --profile flag takes precedence over the profile in the databricks.yml file
cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("PROFILE-2")
b := setupWithProfile(t, cmd, "PROFILE-1")
client, err := b.InitializeWorkspaceClient()
require.NoError(t, err)
assert.Equal(t, "https://a.com", client.Config.Host)
assert.Equal(t, "b", client.Config.Token)
assert.Equal(t, "PROFILE-2", client.Config.Profile)
}
func TestBundleConfigureProfileEnvVariable(t *testing.T) {
testutil.CleanupEnvironment(t)
// The DATABRICKS_CONFIG_PROFILE environment variable takes precedence over the profile in the databricks.yml file
t.Setenv("DATABRICKS_CONFIG_PROFILE", "PROFILE-2")
cmd := emptyCommand(t)
b := setupWithProfile(t, cmd, "PROFILE-1")
client, err := b.InitializeWorkspaceClient()
require.NoError(t, err)
assert.Equal(t, "https://a.com", client.Config.Host)
assert.Equal(t, "b", client.Config.Token)
assert.Equal(t, "PROFILE-2", client.Config.Profile)
}
func TestBundleConfigureProfileFlagAndEnvVariable(t *testing.T) {
testutil.CleanupEnvironment(t)
// The --profile flag takes precedence over the DATABRICKS_CONFIG_PROFILE environment variable
t.Setenv("DATABRICKS_CONFIG_PROFILE", "NOEXIST")
cmd := emptyCommand(t)
cmd.Flag("profile").Value.Set("PROFILE-2")
b := setupWithProfile(t, cmd, "PROFILE-1")
client, err := b.InitializeWorkspaceClient()
require.NoError(t, err)
assert.Equal(t, "https://a.com", client.Config.Host)
assert.Equal(t, "b", client.Config.Token)
assert.Equal(t, "PROFILE-2", client.Config.Profile)
} }
func TestTargetFlagFull(t *testing.T) { func TestTargetFlagFull(t *testing.T) {
@ -149,7 +224,7 @@ func TestTargetFlagFull(t *testing.T) {
err := cmd.ExecuteContext(ctx) err := cmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, getTarget(cmd), "development") assert.Equal(t, "development", getTarget(cmd))
} }
func TestTargetFlagShort(t *testing.T) { func TestTargetFlagShort(t *testing.T) {
@ -161,7 +236,7 @@ func TestTargetFlagShort(t *testing.T) {
err := cmd.ExecuteContext(ctx) err := cmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, getTarget(cmd), "production") assert.Equal(t, "production", getTarget(cmd))
} }
// TODO: remove when environment flag is fully deprecated // TODO: remove when environment flag is fully deprecated
@ -175,5 +250,5 @@ func TestTargetEnvironmentFlag(t *testing.T) {
err := cmd.ExecuteContext(ctx) err := cmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, getTarget(cmd), "development") assert.Equal(t, "development", getTarget(cmd))
} }