diff --git a/.changes/unreleased/ENHANCEMENTS-20240829-140210.yaml b/.changes/unreleased/ENHANCEMENTS-20240829-140210.yaml new file mode 100644 index 00000000..c3bdc561 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240829-140210.yaml @@ -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 diff --git a/go.mod b/go.mod index 423c6f67..09ee91f6 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9e10bae8..8de3c036 100644 --- a/go.sum +++ b/go.sum @@ -233,8 +233,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= diff --git a/internal/features/stacks/decoder/path_reader.go b/internal/features/stacks/decoder/path_reader.go index f383280b..f3b649d3 100644 --- a/internal/features/stacks/decoder/path_reader.go +++ b/internal/features/stacks/decoder/path_reader.go @@ -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) @@ -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) diff --git a/internal/features/stacks/jobs/builtin_references.go b/internal/features/stacks/jobs/builtin_references.go new file mode 100644 index 00000000..8fb7072f --- /dev/null +++ b/internal/features/stacks/jobs/builtin_references.go @@ -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)), + }) +} diff --git a/internal/features/stacks/jobs/references.go b/internal/features/stacks/jobs/references.go index a6a8c4bf..244161bb 100644 --- a/internal/features/stacks/jobs/references.go +++ b/internal/features/stacks/jobs/references.go @@ -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 { diff --git a/internal/features/stacks/state/stack_meta.go b/internal/features/stacks/state/stack_meta.go index 34cf9f72..9ffd20d2 100644 --- a/internal/features/stacks/state/stack_meta.go +++ b/internal/features/stacks/state/stack_meta.go @@ -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 { @@ -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 } diff --git a/internal/features/stacks/state/stack_store.go b/internal/features/stacks/state/stack_store.go index 7c27dccc..e861ca89 100644 --- a/internal/features/stacks/state/stack_store.go +++ b/internal/features/stacks/state/stack_store.go @@ -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