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

Added bundle deployment bind and unbind command #1131

Merged
merged 18 commits into from
Feb 14, 2024
Merged
22 changes: 22 additions & 0 deletions bundle/config/resources.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package config

import (
"context"
"fmt"

"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/databricks-sdk-go"
)

// Resources defines Databricks resources associated with the bundle.
Expand Down Expand Up @@ -168,3 +170,23 @@ func (r *Resources) Merge() error {
}
return nil
}

type ConfigResource interface {
Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) bool
TerraformResourceName() string
}

func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) {
for k := range r.Jobs {
if k == key {
return r.Jobs[k], nil
}
}
for k := range r.Pipelines {
if k == key {
return r.Pipelines[k], nil
}
}

return nil, fmt.Errorf("no such resource: %s", key)
}
19 changes: 19 additions & 0 deletions bundle/config/resources/job.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package resources

import (
"context"
"strconv"

"github.com/databricks/cli/bundle/config/paths"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/marshal"
"github.com/databricks/databricks-sdk-go/service/jobs"
"github.com/imdario/mergo"
Expand Down Expand Up @@ -90,3 +94,18 @@ func (j *Job) MergeTasks() error {
j.Tasks = tasks
return nil
}

func (j *Job) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) bool {
jobId, err := strconv.Atoi(id)
if err != nil {
return false
}
_, err = w.Jobs.Get(ctx, jobs.GetJobRequest{
JobId: int64(jobId),
})
return err == nil
}

func (j *Job) TerraformResourceName() string {
return "databricks_job"
}
13 changes: 13 additions & 0 deletions bundle/config/resources/pipeline.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package resources

import (
"context"
"strings"

"github.com/databricks/cli/bundle/config/paths"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/marshal"
"github.com/databricks/databricks-sdk-go/service/pipelines"
"github.com/imdario/mergo"
Expand Down Expand Up @@ -73,3 +75,14 @@ func (p *Pipeline) MergeClusters() error {
p.Clusters = output
return nil
}

func (p *Pipeline) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) bool {
_, err := w.Pipelines.Get(ctx, pipelines.GetPipelineRequest{
PipelineId: id,
})
return err == nil
}

func (p *Pipeline) TerraformResourceName() string {
return "databricks_pipeline"
}
4 changes: 4 additions & 0 deletions bundle/deploy/lock/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
type Goal string

const (
GoalBind = Goal("bind")
GoalUnbind = Goal("unbind")
GoalDeploy = Goal("deploy")
GoalDestroy = Goal("destroy")
)
Expand Down Expand Up @@ -46,6 +48,8 @@ func (m *release) Apply(ctx context.Context, b *bundle.Bundle) error {
switch m.goal {
case GoalDeploy:
return b.Locker.Unlock(ctx)
case GoalBind, GoalUnbind:
return b.Locker.Unlock(ctx)
case GoalDestroy:
return b.Locker.Unlock(ctx, locker.AllowLockFileNotExist)
default:
Expand Down
129 changes: 129 additions & 0 deletions bundle/deploy/terraform/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package terraform

import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/log"
"github.com/hashicorp/terraform-exec/tfexec"
)

type BindOptions struct {
AutoApprove bool
ResourceType string
ResourceKey string
ResourceId string
}

type importResource struct {
opts *BindOptions
}

// Apply implements bundle.Mutator.
func (m *importResource) Apply(ctx context.Context, b *bundle.Bundle) error {
tf := b.Terraform
if tf == nil {
return fmt.Errorf("terraform not initialized")
}

err := tf.Init(ctx, tfexec.Upgrade(true))
if err != nil {
return fmt.Errorf("terraform init: %w", err)
}

importsFilePath, err := m.writeImportsFile(ctx, b)
if err != nil {
return fmt.Errorf("write imports file: %w", err)
}
log.Debugf(ctx, "imports.tf file written to %s", importsFilePath)

buf := bytes.NewBuffer(nil)
tf.SetStdout(buf)
changed, err := tf.Plan(ctx)
if err != nil {
return fmt.Errorf("terraform plan: %w", err)
}

if changed && !m.opts.AutoApprove {
cmdio.LogString(ctx, buf.String())
ans, err := cmdio.AskYesOrNo(ctx, "Confirm import changes? Changes will be remotely only after running 'bundle deploy'.")
if err != nil {
return err
}
if !ans {
// remove imports.tf file
_ = os.Remove(importsFilePath)
return fmt.Errorf("import aborted")
}
}

log.Debugf(ctx, "resource imports approved")
return nil
}

func (m *importResource) writeImportsFile(ctx context.Context, b *bundle.Bundle) (string, error) {
// Write imports.tf file to the terraform root directory
dir, err := Dir(ctx, b)
if err != nil {
return "", err
}

importsFilePath := filepath.Join(dir, "imports.tf")
f, err := os.Create(importsFilePath)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf(`import {
to = %s.%s
id = "%s"
}`, m.opts.ResourceType, m.opts.ResourceKey, m.opts.ResourceId))

return importsFilePath, err
}

// Name implements bundle.Mutator.
func (*importResource) Name() string {
return "terraform.Import"
}

func Import(opts *BindOptions) bundle.Mutator {
return &importResource{opts: opts}
}

type unbind struct {
resourceType string
resourceKey string
}

func (m *unbind) Apply(ctx context.Context, b *bundle.Bundle) error {
tf := b.Terraform
if tf == nil {
return fmt.Errorf("terraform not initialized")
}

err := tf.Init(ctx, tfexec.Upgrade(true))
if err != nil {
return fmt.Errorf("terraform init: %w", err)
}

err = tf.StateRm(ctx, fmt.Sprintf("%s.%s", m.resourceType, m.resourceKey))
if err != nil {
return fmt.Errorf("terraform state rm: %w", err)
}

return nil
}

func (*unbind) Name() string {
return "terraform.Unbind"
}

func Unbind(resourceType string, resourceKey string) bundle.Mutator {
return &unbind{resourceType: resourceType, resourceKey: resourceKey}
}
44 changes: 44 additions & 0 deletions bundle/phases/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package phases

import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/deploy/lock"
"github.com/databricks/cli/bundle/deploy/terraform"
)

func Bind(opts *terraform.BindOptions) bundle.Mutator {
return newPhase(
"bind",
[]bundle.Mutator{
lock.Acquire(),
bundle.Defer(
bundle.Seq(
terraform.Interpolate(),
terraform.Write(),
terraform.StatePull(),
terraform.Import(opts),
),
lock.Release(lock.GoalBind),
),
},
)
}

func Unbind(resourceType string, resourceKey string) bundle.Mutator {
return newPhase(
"unbind",
[]bundle.Mutator{
lock.Acquire(),
bundle.Defer(
bundle.Seq(
terraform.Interpolate(),
terraform.Write(),
terraform.StatePull(),
terraform.Unbind(resourceType, resourceKey),
terraform.StatePush(),
),
lock.Release(lock.GoalUnbind),
),
},
)
}
2 changes: 2 additions & 0 deletions cmd/bundle/bundle.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bundle

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

Expand All @@ -24,5 +25,6 @@ func New() *cobra.Command {
cmd.AddCommand(newInitCommand())
cmd.AddCommand(newSummaryCommand())
cmd.AddCommand(newGenerateCommand())
cmd.AddCommand(deployment.NewDeploymentCommand())
return cmd
}
3 changes: 2 additions & 1 deletion cmd/bundle/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package bundle
import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/phases"
"github.com/databricks/cli/cmd/bundle/utils"
"github.com/spf13/cobra"
)

func newDeployCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy bundle",
PreRunE: ConfigureBundleWithVariables,
PreRunE: utils.ConfigureBundleWithVariables,
}

var force bool
Expand Down
53 changes: 53 additions & 0 deletions cmd/bundle/deployment/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package deployment

import (
"fmt"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/deploy/terraform"
"github.com/databricks/cli/bundle/phases"
"github.com/databricks/cli/cmd/bundle/utils"
"github.com/spf13/cobra"
)

func newBindCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "bind KEY RESOURCE_ID",
Short: "Bind bundle-defined resources to existing resources",
Args: cobra.ExactArgs(2),
PreRunE: utils.ConfigureBundleWithVariables,
}

var autoApprove bool
var forceLock bool
cmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Automatically approve the binding")
cmd.Flags().BoolVar(&forceLock, "force-lock", false, "Force acquisition of deployment lock.")

cmd.RunE = func(cmd *cobra.Command, args []string) error {
b := bundle.Get(cmd.Context())
r := b.Config.Resources
resource, err := r.FindResourceByConfigKey(args[0])
if err != nil {
return err
}

w := b.WorkspaceClient()
ctx := cmd.Context()
if !resource.Exists(ctx, w, args[1]) {
return fmt.Errorf("%s with an id '%s' is not found", resource.TerraformResourceName(), args[1])
}

b.Config.Bundle.Lock.Force = forceLock
return bundle.Apply(cmd.Context(), b, bundle.Seq(
phases.Initialize(),
phases.Bind(&terraform.BindOptions{
AutoApprove: autoApprove,
ResourceType: resource.TerraformResourceName(),
ResourceKey: args[0],
ResourceId: args[1],
}),
))
Copy link
Contributor

Choose a reason for hiding this comment

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

If this succeeds we should display a message (if in text output mode) to confirm it did.

This can also include a call to action to hint at running a deploy next.

Copy link
Contributor

Choose a reason for hiding this comment

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

@juliacrawf-db Could you shine your light on how to best convey this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh no! I missed this ping! That string looks great. But wouldn't we want a similar error message in unbind.go if the unbinding fails?

}

return cmd
}
17 changes: 17 additions & 0 deletions cmd/bundle/deployment/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package deployment

import (
"github.com/spf13/cobra"
)

func NewDeploymentCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deployment",
Short: "Deployment related commands",
Long: "Deployment related commands",
}

cmd.AddCommand(newBindCommand())
cmd.AddCommand(newUnbindCommand())
return cmd
}
Loading
Loading