mirror of https://github.com/databricks/cli.git
[WIP] Show plan on bricks bundle deploy
This commit is contained in:
parent
598ad62688
commit
c5497d39d6
|
@ -16,18 +16,21 @@ func (m *delete) Name() string {
|
|||
return "files.Delete"
|
||||
}
|
||||
|
||||
// TODO: autoapprove and tty detection for destroy. Don't allow destroy without auto-approve otherwise. Note this is a breaking change
|
||||
|
||||
func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
|
||||
// Do not delete files if terraform destroy was not consented
|
||||
if !b.Plan.IsEmpty && !b.Plan.ConfirmApply {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Starting deletion of remote bundle files")
|
||||
cmdio.LogString(ctx, "\nStarting deletion of remote bundle files")
|
||||
cmdio.LogString(ctx, fmt.Sprintf("Bundle remote directory is %s", b.Config.Workspace.RootPath))
|
||||
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
if !b.AutoApprove {
|
||||
proceed, err := cmdio.Ask(ctx, fmt.Sprintf("\n%s and all files in it will be %s Proceed?: ", b.Config.Workspace.RootPath, red("deleted permanently!")))
|
||||
cmdio.LogString(ctx, fmt.Sprintf("\n%s and all files in it will be %s.", b.Config.Workspace.RootPath, red("deleted permanently!")))
|
||||
proceed, err := cmdio.Ask(ctx, "Proceed with deletion?: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -54,7 +57,7 @@ func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, fmt.Sprintf("Deleted snapshot file at %s", sync.SnapshotPath()))
|
||||
cmdio.LogString(ctx, fmt.Sprintf("\n Deleted snapshot file at %s", sync.SnapshotPath()))
|
||||
cmdio.LogString(ctx, "Successfully deleted files!")
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func (m *upload) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, fmt.Sprintf("Uploaded bundle files at %s!\n", b.Config.Workspace.FilesPath))
|
||||
cmdio.LogString(ctx, fmt.Sprintf("Uploaded bundle files at %s\n", b.Config.Workspace.FilesPath))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -6,39 +6,93 @@ import (
|
|||
|
||||
"github.com/databricks/bricks/bundle"
|
||||
"github.com/databricks/bricks/libs/cmdio"
|
||||
"github.com/fatih/color"
|
||||
"github.com/hashicorp/terraform-exec/tfexec"
|
||||
)
|
||||
|
||||
type apply struct{}
|
||||
type ApplyGoal string
|
||||
|
||||
var (
|
||||
ApplyDeploy = ApplyGoal("deploy")
|
||||
ApplyDestroy = ApplyGoal("destroy")
|
||||
)
|
||||
|
||||
type apply struct {
|
||||
goal ApplyGoal
|
||||
}
|
||||
|
||||
func (w *apply) Name() string {
|
||||
return "terraform.Apply"
|
||||
}
|
||||
|
||||
func (w *apply) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
|
||||
// return early if plan is empty
|
||||
if b.Plan.IsEmpty {
|
||||
if w.goal == ApplyDeploy {
|
||||
cmdio.LogString(ctx, "No resource changes to apply. Skipping deploy!")
|
||||
}
|
||||
if w.goal == ApplyDestroy {
|
||||
cmdio.LogString(ctx, "No resources to destroy in plan. Skipping destroy!")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tf := b.Terraform
|
||||
if tf == nil {
|
||||
return nil, fmt.Errorf("terraform not initialized")
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Starting resource deployment")
|
||||
|
||||
err := tf.Init(ctx, tfexec.Upgrade(true))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("terraform init: %w", err)
|
||||
}
|
||||
|
||||
err = tf.Apply(ctx)
|
||||
// Ask for confirmation, if needed
|
||||
if !b.Plan.ConfirmApply {
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
if b.Plan.IsReplacingResource {
|
||||
cmdio.LogString(ctx, fmt.Sprintf("One or more resources will be %s. Any previous metadata associated might be lost.", yellow("replaced")))
|
||||
}
|
||||
if b.Plan.IsDeletingResource {
|
||||
cmdio.LogString(ctx, fmt.Sprintf("One or more resources will be permanently %s", red("destroyed")))
|
||||
}
|
||||
b.Plan.ConfirmApply, err = cmdio.Ask(ctx, "Proceed with apply? [y/n]: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// return if confirmation was not provided
|
||||
if !b.Plan.ConfirmApply {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if b.Plan.Path == "" {
|
||||
return nil, fmt.Errorf("no plan found")
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "\nStarting resource deployment")
|
||||
|
||||
// Apply terraform according to the computed destroy plan
|
||||
err = tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Resource deployment completed!")
|
||||
if w.goal == ApplyDeploy {
|
||||
cmdio.LogString(ctx, "Successfully deployed resources!")
|
||||
}
|
||||
if w.goal == ApplyDestroy {
|
||||
cmdio.LogString(ctx, "Successfully destroyed resources!")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Apply returns a [bundle.Mutator] that runs the equivalent of `terraform apply`
|
||||
// from the bundle's ephemeral working directory for Terraform.
|
||||
func Apply() bundle.Mutator {
|
||||
return &apply{}
|
||||
func Apply(goal ApplyGoal) bundle.Mutator {
|
||||
return &apply{
|
||||
goal: goal,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/databricks/bricks/bundle"
|
||||
"github.com/databricks/bricks/libs/cmdio"
|
||||
"github.com/hashicorp/terraform-exec/tfexec"
|
||||
)
|
||||
|
||||
type showPlan struct{}
|
||||
|
||||
func (m *showPlan) Name() string {
|
||||
return "terraform.ShowPlan"
|
||||
}
|
||||
|
||||
func (m *showPlan) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
|
||||
tf := b.Terraform
|
||||
if tf == nil {
|
||||
return nil, fmt.Errorf("terraform not initialized")
|
||||
}
|
||||
|
||||
err := tf.Init(ctx, tfexec.Upgrade(true))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("terraform init: %w", err)
|
||||
}
|
||||
|
||||
// read plan file
|
||||
plan, err := tf.ShowPlanFile(ctx, b.Plan.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// compute bundle specific change events
|
||||
changeEvents := make([]*ResourceChangeEvent, 0)
|
||||
for _, change := range plan.ResourceChanges {
|
||||
if change.Change == nil {
|
||||
continue
|
||||
}
|
||||
if change.Change.Actions.Replace() {
|
||||
b.Plan.IsReplacingResource = true
|
||||
}
|
||||
if change.Change.Actions.Delete() {
|
||||
b.Plan.IsDeletingResource = true
|
||||
}
|
||||
event := toResourceChangeEvent(change)
|
||||
if event == nil {
|
||||
continue
|
||||
}
|
||||
changeEvents = append(changeEvents, event)
|
||||
}
|
||||
|
||||
// return without logging anything if no relevant change events in computed plan
|
||||
if len(changeEvents) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// log resource changes
|
||||
cmdio.LogString(ctx, "The following resource changes will be applied:")
|
||||
for _, event := range changeEvents {
|
||||
cmdio.Log(ctx, event)
|
||||
}
|
||||
cmdio.LogNewline(ctx)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ShowPlan() bundle.Mutator {
|
||||
return &showPlan{}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
tfjson "github.com/hashicorp/terraform-json"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type ResourceChangeEvent struct {
|
||||
Name string `json:"name"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func toAction(actions tfjson.Actions) string {
|
||||
action := "no-op"
|
||||
switch {
|
||||
case actions.Create():
|
||||
action = "create"
|
||||
case actions.Read():
|
||||
action = "read"
|
||||
case actions.Update():
|
||||
action = "update"
|
||||
case actions.Delete():
|
||||
action = "delete"
|
||||
case actions.Replace():
|
||||
action = "replace"
|
||||
}
|
||||
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
isTty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||
if isTty && action == "create" {
|
||||
action = green(action)
|
||||
}
|
||||
if isTty && action == "delete" {
|
||||
action = red(action)
|
||||
}
|
||||
if isTty && action == "replace" {
|
||||
action = yellow(action)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func toResourceType(terraformType string) string {
|
||||
switch terraformType {
|
||||
case "databricks_job":
|
||||
return "job"
|
||||
case "databricks_pipeline":
|
||||
return "pipeline"
|
||||
case "databricks_mlflow_model":
|
||||
return "mlflow_model"
|
||||
case "databricks_mlflow_experiment":
|
||||
return "mlflow_experiment"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toResourceChangeEvent(change *tfjson.ResourceChange) *ResourceChangeEvent {
|
||||
if change.Change == nil {
|
||||
return nil
|
||||
}
|
||||
actions := change.Change.Actions
|
||||
if actions.Read() || actions.NoOp() {
|
||||
return nil
|
||||
}
|
||||
action := toAction(actions)
|
||||
|
||||
resourceType := toResourceType(change.Type)
|
||||
if resourceType == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := change.Name
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ResourceChangeEvent{
|
||||
Name: name,
|
||||
Action: action,
|
||||
ResourceType: resourceType,
|
||||
}
|
||||
}
|
||||
|
||||
func (event *ResourceChangeEvent) String() string {
|
||||
return strings.Join([]string{" ", string(event.Action), event.ResourceType, event.Name}, " ")
|
||||
}
|
||||
|
||||
func (event *ResourceChangeEvent) IsInplaceSupported() bool {
|
||||
return false
|
||||
}
|
|
@ -19,7 +19,9 @@ func Deploy() bundle.Mutator {
|
|||
terraform.Interpolate(),
|
||||
terraform.Write(),
|
||||
terraform.StatePull(),
|
||||
terraform.Apply(),
|
||||
terraform.Plan("deploy"),
|
||||
terraform.ShowPlan(),
|
||||
terraform.Apply("deploy"),
|
||||
terraform.StatePush(),
|
||||
lock.Release(),
|
||||
},
|
||||
|
|
|
@ -14,8 +14,9 @@ func Destroy() bundle.Mutator {
|
|||
[]bundle.Mutator{
|
||||
lock.Acquire(),
|
||||
terraform.StatePull(),
|
||||
terraform.Plan(terraform.PlanGoal("destroy")),
|
||||
terraform.Destroy(),
|
||||
terraform.Plan("destroy"),
|
||||
terraform.ShowPlan(),
|
||||
terraform.Apply("destroy"),
|
||||
terraform.StatePush(),
|
||||
lock.Release(),
|
||||
files.Delete(),
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
package bundle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/databricks/bricks/bundle"
|
||||
"github.com/databricks/bricks/bundle/phases"
|
||||
"github.com/databricks/bricks/cmd/root"
|
||||
"github.com/databricks/bricks/libs/cmdio"
|
||||
"github.com/databricks/bricks/libs/flags"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var deployCmd = &cobra.Command{
|
||||
|
@ -13,11 +19,30 @@ var deployCmd = &cobra.Command{
|
|||
|
||||
PreRunE: root.MustConfigureBundle,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
b := bundle.Get(cmd.Context())
|
||||
|
||||
// If `--force` is specified, force acquisition of the deployment lock.
|
||||
b.Config.Bundle.Lock.Force = force
|
||||
|
||||
// If `--auto-approve`` is specified, we skip confirmation checks
|
||||
b.AutoApprove = autoApprove
|
||||
|
||||
// we require auto-approve for non tty terminals since interactive consent
|
||||
// is not possible
|
||||
if !term.IsTerminal(int(os.Stderr.Fd())) && !autoApprove {
|
||||
return fmt.Errorf("please specify --auto-approve to skip interactive confirmation checks for non tty consoles")
|
||||
}
|
||||
|
||||
// Check auto-approve is selected for json logging
|
||||
logger, ok := cmdio.FromContext(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("progress logger not found")
|
||||
}
|
||||
if logger.Mode == flags.ModeJson && !autoApprove {
|
||||
return fmt.Errorf("please specify --auto-approve since selected logging format is json")
|
||||
}
|
||||
|
||||
return bundle.Apply(cmd.Context(), b, []bundle.Mutator{
|
||||
phases.Initialize(),
|
||||
phases.Build(),
|
||||
|
@ -28,7 +53,10 @@ var deployCmd = &cobra.Command{
|
|||
|
||||
var force bool
|
||||
|
||||
var autoApprove bool
|
||||
|
||||
func init() {
|
||||
AddCommand(deployCmd)
|
||||
deployCmd.Flags().BoolVar(&force, "force", false, "Force acquisition of deployment lock.")
|
||||
deployCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Skip interactive approvals")
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// TODO: note possibility of aliases
|
||||
|
||||
var destroyCmd = &cobra.Command{
|
||||
Use: "destroy",
|
||||
Short: "Destroy deployed bundle resources",
|
||||
|
@ -51,9 +53,8 @@ var destroyCmd = &cobra.Command{
|
|||
},
|
||||
}
|
||||
|
||||
var autoApprove bool
|
||||
|
||||
func init() {
|
||||
AddCommand(destroyCmd)
|
||||
destroyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Skip interactive approvals for deleting resources and files")
|
||||
destroyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Skip interactive approvals")
|
||||
destroyCmd.Flags().BoolVar(&force, "force", false, "Force acquisition of deployment lock.")
|
||||
}
|
||||
|
|
|
@ -63,6 +63,14 @@ func LogString(ctx context.Context, message string) {
|
|||
})
|
||||
}
|
||||
|
||||
func LogNewline(ctx context.Context) {
|
||||
logger, ok := FromContext(ctx)
|
||||
if !ok {
|
||||
logger = Default()
|
||||
}
|
||||
logger.Log(&NewlineEvent{})
|
||||
}
|
||||
|
||||
func Ask(ctx context.Context, question string) (bool, error) {
|
||||
logger, ok := FromContext(ctx)
|
||||
if !ok {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package cmdio
|
||||
|
||||
type NewlineEvent struct{}
|
||||
|
||||
func (event *NewlineEvent) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (event *NewlineEvent) IsInplaceSupported() bool {
|
||||
return false
|
||||
}
|
|
@ -10,4 +10,10 @@ type Plan struct {
|
|||
|
||||
// If true, the plan is empty and applying it will not do anything
|
||||
IsEmpty bool
|
||||
|
||||
// If true, there are one or more resources in plan that will be re-created
|
||||
IsReplacingResource bool
|
||||
|
||||
// if true, there are one or more resources in plan that will be destroyed
|
||||
IsDeletingResource bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue