Skip to content

Commit

Permalink
(TFECO-7339) feat: Support context references within orchestrate bloc…
Browse files Browse the repository at this point in the history
…ks in deployment configuration (#1813)

* feat: Support context references within orchestrate blocks in deployment configuration

* chore: add changie entry

* Bump terraform-schema to `c383931`

* refactor: simplify code
  • Loading branch information
ansgarm authored Aug 30, 2024
1 parent 264bb48 commit 02e7181
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20240829-140210.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: ENHANCEMENTS
body: Support context references within orchestrate blocks in deployment configuration
time: 2024-08-29T14:02:10.75361+02:00
custom:
Issue: "1813"
Repository: terraform-ls
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/hashicorp/terraform-exec v0.21.0
github.com/hashicorp/terraform-json v0.22.1
github.com/hashicorp/terraform-registry-address v0.2.3
github.com/hashicorp/terraform-schema v0.0.0-20240826132342-4f99bab76318
github.com/hashicorp/terraform-schema v0.0.0-20240830131621-c383931da2df
github.com/mcuadros/go-defaults v1.2.0
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.1.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-schema v0.0.0-20240826132342-4f99bab76318 h1:uvRBiaf+0qM3c6u/AjOmKsvdtEsSKlmty8PJ9ukEnsY=
github.com/hashicorp/terraform-schema v0.0.0-20240826132342-4f99bab76318/go.mod h1:Tc8mlcXI3ulpnC1/Ho4O5DeivcXGfezj0U+igIDE3iA=
github.com/hashicorp/terraform-schema v0.0.0-20240830131621-c383931da2df h1:RALMHz4If9EjrEraJ88CbyO8zxLGXtmYlbLAtpMmNxQ=
github.com/hashicorp/terraform-schema v0.0.0-20240830131621-c383931da2df/go.mod h1:Tc8mlcXI3ulpnC1/Ho4O5DeivcXGfezj0U+igIDE3iA=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo=
Expand Down
2 changes: 2 additions & 0 deletions internal/features/stacks/decoder/path_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func stackPathContext(record *state.StackRecord, stateReader CombinedReader) (*d
Filenames: record.Meta.Filenames,
Deployments: record.Meta.Deployments,
Stores: record.Meta.Stores,
OrchestrationRules: record.Meta.OrchestrationRules,
}

mergedSchema, err := sm.SchemaForStack(meta)
Expand Down Expand Up @@ -159,6 +160,7 @@ func deployPathContext(record *state.StackRecord) (*decoder.PathContext, error)
Filenames: record.Meta.Filenames,
Deployments: record.Meta.Deployments,
Stores: record.Meta.Stores,
OrchestrationRules: record.Meta.OrchestrationRules,
}

mergedSchema, err := sm.SchemaForDeployment(meta)
Expand Down
213 changes: 213 additions & 0 deletions internal/features/stacks/jobs/builtin_references.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package jobs

import (
"fmt"

"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-ls/internal/features/stacks/state"
"github.com/zclconf/go-cty/cty"
)

var orchestrateContextScopeId = lang.ScopeId("orchestrate_context")

// used in various places
var changesAttributes = map[string]cty.Type{
"total": cty.Number,
"add": cty.Number,
"change": cty.Number,
"import": cty.Number,
"remove": cty.Number,
"move": cty.Number,
"forget": cty.Number,
"defer": cty.Number,
}
var changesType = cty.Object(changesAttributes)

func builtinReferences(record *state.StackRecord) reference.Targets {
targets := make(reference.Targets, 0)

if record == nil {
return targets
}

// The ranges of the orchestrate blocks as we have to create targets with these ranges
// to ensure they are only available within orchestrate blocks
for _, rule := range record.Meta.OrchestrationRules {
rng := rule.Range

// create the static base targets (like context.operation, context.success, etc.)
targets = append(targets, baseTargets(rng)...)
// create the static plan targets (like context.plan.mode, context.plan.applyable, etc.)
targets = append(targets, staticPlanTargets(rng)...)

// targets for each component for the component_changes map (like context.plan.component_changes["vpc"].total)
for name := range record.Meta.Components {
addr := lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "component_changes"},
lang.IndexStep{Key: cty.StringVal(name)},
}
targets = append(targets, changesTargets(addr, rng, &name)...)
}
}

return targets
}

func baseTargets(rng hcl.Range) reference.Targets {
var diagType = cty.Object(map[string]cty.Type{
"summary": cty.String,
"detail": cty.String,
})

return reference.Targets{
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "operation"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.String,
Description: lang.Markdown("The operation. Either \"plan\" or \"apply\""),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "success"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Bool,
Description: lang.Markdown("Whether the operation that triggered the evaluation of this check completed successfully"),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "errors"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Set(diagType),
Description: lang.Markdown("A set of diagnostic error message objects"),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "warnings"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Set(diagType),
Description: lang.Markdown("A set of diagnostic warning message objects"),
},
}
}

// staticPlanTargets returns the targets for the plan context that are not dependent on the component names
func staticPlanTargets(rng hcl.Range) reference.Targets {
targets := reference.Targets{
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Object(map[string]cty.Type{
"mode": cty.String,
"applyable": cty.Bool,
"changes": changesType,
"component_changes": cty.Map(changesType),
"replans": cty.Number,
"deployment": cty.DynamicPseudoType,
}),
Description: lang.Markdown("An object including data about the current plan"),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "mode"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.String,
Description: lang.Markdown("The plan mode, one of \"normal\", \"refresh-only\", or \"destroy\""),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "applyable"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Bool,
Description: lang.Markdown("A boolean, whether or not the plan can be applied"),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "replans"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.Number,
Description: lang.Markdown("The number of replans in this plan's sequence, starting at 0"),
},
{
LocalAddr: lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "deployment"},
},
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: cty.DynamicPseudoType,
Description: lang.Markdown("A direct reference to the current deployment. Can be used to compare with deployments blocks, e.g. context.plan.deployment == deployment.production"),
},
}
// utility to add all the changes targets like context.plan.changes.total, context.plan.changes.add, etc.
targets = append(targets, changesTargets(lang.Address{
lang.RootStep{Name: "context"},
lang.AttrStep{Name: "plan"},
lang.AttrStep{Name: "changes"},
}, rng, nil)...)
return targets
}

func changesTargets(address lang.Address, rng hcl.Range, componentName *string) reference.Targets {
descriptionAppendix := "for all components" // default
if componentName != nil {
descriptionAppendix = fmt.Sprintf("for the component \"%s\"", *componentName)
}

nestedTargets := make(reference.Targets, 0)
for key, typ := range changesAttributes {
a := append(address.Copy(), lang.AttrStep{Name: key})
nestedTargets = append(nestedTargets, reference.Target{
Name: key,
LocalAddr: a,
TargetableFromRangePtr: rng.Ptr(),
ScopeId: orchestrateContextScopeId,
Type: typ,
Description: lang.Markdown(fmt.Sprintf("The number of %s changes %s", key, descriptionAppendix)),
})
}

return append(nestedTargets, reference.Target{
LocalAddr: address,
TargetableFromRangePtr: rng.Ptr(),
Type: changesType,
Name: "changes",
Description: lang.Markdown(fmt.Sprintf("The changes that are planned %s", descriptionAppendix)),
})
}
7 changes: 7 additions & 0 deletions internal/features/stacks/jobs/references.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ func DecodeReferenceTargets(ctx context.Context, stackStore *state.StackStore, m
}
deployTargets, rErr := deployDecoder.CollectReferenceTargets()

record, err := stackStore.StackRecordByPath(stackPath)
if err != nil {
return err
}
builtinTargets := builtinReferences(record)

targets := make(reference.Targets, 0)
targets = append(targets, stackTargets...)
targets = append(targets, deployTargets...)
targets = append(targets, builtinTargets...)

sErr := stackStore.UpdateReferenceTargets(stackPath, targets, rErr)
if sErr != nil {
Expand Down
12 changes: 10 additions & 2 deletions internal/features/stacks/state/stack_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type StackMetadata struct {
Outputs map[string]tfstack.Output
ProviderRequirements map[string]tfstack.ProviderRequirement

Deployments map[string]tfstack.Deployment
Stores map[string]tfstack.Store
Deployments map[string]tfstack.Deployment
Stores map[string]tfstack.Store
OrchestrationRules map[string]tfstack.OrchestrationRule
}

func (sm StackMetadata) Copy() StackMetadata {
Expand Down Expand Up @@ -67,5 +68,12 @@ func (sm StackMetadata) Copy() StackMetadata {
}
}

if sm.OrchestrationRules != nil {
newSm.OrchestrationRules = make(map[string]tfstack.OrchestrationRule, len(sm.OrchestrationRules))
for k, v := range sm.OrchestrationRules {
newSm.OrchestrationRules[k] = v
}
}

return newSm
}
1 change: 1 addition & 0 deletions internal/features/stacks/state/stack_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func (s *StackStore) UpdateMetadata(path string, meta *tfstack.Meta, mErr error)
ProviderRequirements: meta.ProviderRequirements,
Deployments: meta.Deployments,
Stores: meta.Stores,
OrchestrationRules: meta.OrchestrationRules,
}
record.MetaErr = mErr

Expand Down

0 comments on commit 02e7181

Please sign in to comment.