Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions bundle/deploy/metadata/compute.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@ import (
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/metadata"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/log"
)

type compute struct{}
Expand All@@ -20,7 +21,7 @@ func (m *compute) Name() string {
return "metadata.Compute"
}

func (m *compute) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
func (m *compute) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
b.Metadata = metadata.Metadata{
Version: metadata.Version,
Config: metadata.Config{},
Expand All@@ -40,10 +41,17 @@ func (m *compute) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
// Compute config file path the job is defined in, relative to the bundle
// root
l := b.Config.GetLocation("resources.jobs." + name)
if l.File == "" {
// b.Config.Resources.Jobs may include a job that only exists in state but not in config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this possible and why only for jobs?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I think that's because we populate ID and URL for all resources (for summary)? I don't have a repro, this maybe triggered under specific conditions, but I've seen this particular error before.

It's only happening for jobs because this code is only run for jobs but other resources would behave the same.

continue
}

relativePath, err := filepath.Rel(b.BundleRootPath, l.File)
if err != nil {
return diag.Errorf("failed to compute relative path for job %s: %v", name, err)
log.Warnf(ctx, "failed to compute relative path for job %q: %s", name, err)
relativePath = ""
}

// Metadata for the job
jobsMetadata[name] = &metadata.Resource{
ID: job.ID,
Expand Down
6 changes: 6 additions & 0 deletions bundle/deployplan/plan.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,3 +97,9 @@ func (p *Plan) UnlockEntry(resourceKey string) {
defer p.mutex.Unlock()
p.locks[resourceKey] = false
}

func (p *Plan) RemoveEntry(resourceKey string) {
p.mutex.Lock()
defer p.mutex.Unlock()
delete(p.Plan, resourceKey)
}
5 changes: 2 additions & 3 deletions bundle/direct/apply.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,7 +93,7 @@ func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState,

func (d *DeploymentUnit) Recreate(ctx context.Context, db *dstate.DeploymentState, oldID string, newState any) error {
err := d.Adapter.DoDelete(ctx, oldID)
if err != nil {
if err != nil && !isResourceGone(err) {
return fmt.Errorf("deleting old id=%s: %w", oldID, err)
}

Expand DownExpand Up@@ -172,9 +172,8 @@ func (d *DeploymentUnit) UpdateWithID(ctx context.Context, db *dstate.Deployment
}

func (d *DeploymentUnit) Delete(ctx context.Context, db *dstate.DeploymentState, oldID string) error {
// TODO: recognize 404 and 403 as "deleted" and proceed to removing state
err := d.Adapter.DoDelete(ctx, oldID)
if err != nil {
if err != nil && !isResourceGone(err) {
return fmt.Errorf("deleting id=%s: %w", oldID, err)
}

Expand Down
2 changes: 1 addition & 1 deletion bundle/direct/bundle_apply.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,7 +161,7 @@ func (b *DeploymentBundle) LookupReferenceRemote(ctx context.Context, path *stru
return structaccess.Get(remoteState, fieldPath)
}

func jsonDump(obj map[string]string) string {
func jsonDump(obj any) string {
bytes, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return err.Error()
Expand Down
129 changes: 59 additions & 70 deletions bundle/direct/bundle_plan.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,13 +4,15 @@ import (
"context"
"errors"
"fmt"
"maps"
"reflect"
"slices"
"strings"

"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/deployplan"
"github.com/databricks/cli/bundle/direct/dresources"
"github.com/databricks/cli/bundle/direct/dstate"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/dyn/dynvar"
"github.com/databricks/cli/libs/log"
Expand All@@ -21,7 +23,6 @@ import (
"github.com/databricks/cli/libs/structs/structvar"
"github.com/databricks/cli/libs/utils"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/apierr"
)

var errDelayed = errors.New("must be resolved after apply")
Expand All@@ -43,14 +44,14 @@ func (b *DeploymentBundle) Init(client *databricks.WorkspaceClient) error {
return err
}

func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) {
func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) {
b.StateDB.AssertOpened()
err := b.Init(client)
if err != nil {
return nil, err
}

plan, err := b.makePlan(ctx, configRoot)
plan, err := b.makePlan(ctx, configRoot, &b.StateDB.Data)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
Expand All@@ -67,7 +68,7 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
return nil, err
}

// We're processing resources in DAG order because we're resolving refernces (that can be resolved at plan stage).
// We're processing resources in DAG order because we're resolving references (that can be resolved at plan stage).
g.Run(defaultParallelism, func(resourceKey string, failedDependency *string) bool {
errorPrefix := "cannot plan " + resourceKey

Expand All@@ -90,6 +91,27 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
return false
}

if entry.Action == deployplan.ActionTypeDelete.String() {
dbentry, hasEntry := b.StateDB.GetResourceEntry(resourceKey)
if !hasEntry {
logdiag.LogError(ctx, fmt.Errorf("%s: internal error, missing in state", errorPrefix))
return false
}

_, err := adapter.DoRefresh(ctx, dbentry.ID)
if err != nil {
if isResourceGone(err) {
// no such resource
plan.RemoveEntry(resourceKey)
} else {
log.Warnf(ctx, "cannot read %s id=%q: %s", resourceKey, dbentry.ID, err)
return false
}
}

return true
}

// Process all references in the resource using Refs map
// Refs maps path inside resource to references e.g. "${resources.jobs.foo.id} ${resources.jobs.foo.name}"
if !b.resolveReferences(ctx, entry, errorPrefix, true) {
Expand DownExpand Up@@ -139,7 +161,7 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d

remoteState, err := adapter.DoRefresh(ctx, dbentry.ID)
if err != nil {
if errors.Is(err, apierr.ErrResourceDoesNotExist) || errors.Is(err, apierr.ErrNotFound) {
if isResourceGone(err) {
remoteState = nil
} else {
logdiag.LogError(ctx, fmt.Errorf("%s: failed to read id=%q: %w (localAction=%q)", errorPrefix, dbentry.ID, err, localAction.String()))
Expand DownExpand Up@@ -188,28 +210,6 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
return nil, errors.New("planning failed")
}

// Note, we cannot simply remove 'skip' entries here as then we'd need to ensure there are no edges to them

state := b.StateDB.ExportState(ctx)

// Remained in state are resources that no longer present in the config
for _, group := range utils.SortedKeys(state) {
_, ok := b.Adapters[group]

if !ok {
log.Warnf(ctx, "%s: resource type not supported on direct backend", group)
continue
}
for _, key := range utils.SortedKeys(state[group]) {
n := "resources." + group + "." + key
_, exists := plan.Plan[n]
if exists {
continue
}
plan.Plan[n] = &deployplan.PlanEntry{Action: deployplan.ActionTypeDelete.String()}
}
}

for _, entry := range plan.Plan {
if entry.Action == deployplan.ActionTypeSkipString {
entry.NewState = nil
Expand DownExpand Up@@ -271,32 +271,6 @@ func interpretOldStateVsRemoteState(ctx context.Context, adapter *dresources.Ada
return action, m
}

func (b *DeploymentBundle) CalculatePlanForDestroy(ctx context.Context, client *databricks.WorkspaceClient) (*deployplan.Plan, error) {
b.StateDB.AssertOpened()

err := b.Init(client)
if err != nil {
return nil, err
}

plan := deployplan.NewPlan()

state := b.StateDB.ExportState(ctx)
for group, groupData := range state {
_, ok := b.Adapters[group]
if !ok {
logdiag.LogError(ctx, fmt.Errorf("cannot destroy %s: resource type not supported on direct backend", group))
continue
}
for key := range groupData {
n := "resources." + group + "." + key
plan.Plan[n] = &deployplan.PlanEntry{Action: deployplan.ActionTypeDelete.String()}
}
}

return plan, nil
}

func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *structpath.PathNode) (any, error) {
targetResourceKey := path.Prefix(3).String()
fieldPath := path.SkipPrefix(3)
Expand DownExpand Up@@ -432,35 +406,41 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
return true
}

func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root) (*deployplan.Plan, error) {
func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root, db *dstate.Database) (*deployplan.Plan, error) {
p := deployplan.NewPlan()

// Collect and sort nodes first, because MapByPattern gives them in randomized order
var nodes []string

// Walk?
_, err := dyn.MapByPattern(
configRoot.Value(),
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
group := p[1].Key()
existingKeys := maps.Clone(db.State)

_, ok := dresources.SupportedResources[group]
if !ok {
return v, fmt.Errorf("unsupported resource: %s", group)
}
// Walk?
if configRoot != nil {
_, err := dyn.MapByPattern(
configRoot.Value(),
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
group := p[1].Key()

_, ok := dresources.SupportedResources[group]
if !ok {
return v, fmt.Errorf("unsupported resource: %s", group)
}

nodes = append(nodes, p.String())
return dyn.InvalidValue, nil
},
)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
nodes = append(nodes, p.String())
return dyn.InvalidValue, nil
},
)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
}

slices.Sort(nodes)

for _, node := range nodes {
delete(existingKeys, node)

prefix := "cannot plan " + node
inputConfig, ok := configRoot.GetResourceConfig(node)
if !ok {
Expand DownExpand Up@@ -541,6 +521,15 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
p.Plan[node] = &e
}

for n := range existingKeys {
if p.Plan[n] != nil {
panic("unexpected node " + n)
}
p.Plan[n] = &deployplan.PlanEntry{
Action: deployplan.ActionTypeDelete.String(),
}
}

return p, nil
}

Expand Down
11 changes: 11 additions & 0 deletions bundle/direct/util.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
package direct

import (
"errors"

"github.com/databricks/databricks-sdk-go/apierr"
)

func isResourceGone(err error) bool {
return errors.Is(err, apierr.ErrResourceDoesNotExist) || errors.Is(err, apierr.ErrNotFound)
}
2 changes: 1 addition & 1 deletion bundle/phases/deploy.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,7 +209,7 @@ func planWithoutPrepare(ctx context.Context, b *bundle.Bundle) *deployplan.Plan
logdiag.LogError(ctx, err)
return nil
}
plan, err := b.DeploymentBundle.CalculatePlanForDeploy(ctx, b.WorkspaceClient(), &b.Config)
plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(), &b.Config)
if err != nil {
logdiag.LogError(ctx, err)
return nil
Expand Down
2 changes: 1 addition & 1 deletion bundle/phases/destroy.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,7 +156,7 @@ func Destroy(ctx context.Context, b *bundle.Bundle) {
logdiag.LogError(ctx, err)
return
}
plan, err = b.DeploymentBundle.CalculatePlanForDestroy(ctx, b.WorkspaceClient())
plan, err = b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(), nil)
if err != nil {
logdiag.LogError(ctx, err)
return
Expand Down
Loading