diff --git a/.changes/unreleased/ENHANCEMENTS-20250117-164515.yaml b/.changes/unreleased/ENHANCEMENTS-20250117-164515.yaml new file mode 100644 index 00000000..c2c5f72a --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250117-164515.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: Report usage of write-only attributes for public providers +time: 2025-01-17T16:45:15.722924+01:00 +custom: + Issue: "1926" + Repository: terraform-ls diff --git a/go.mod b/go.mod index f2c68827..d343a00d 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,12 @@ require ( github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hc-install v0.9.1 - github.com/hashicorp/hcl-lang v0.0.0-20241209140757-4f7c1c9bbc32 + github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.21.0 github.com/hashicorp/terraform-json v0.24.0 github.com/hashicorp/terraform-registry-address v0.2.4 - github.com/hashicorp/terraform-schema v0.0.0-20241212141216-b4693e6bc465 + github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c 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 a3b4589a..bf9bc1a2 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,8 @@ github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXru github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl-lang v0.0.0-20241209140757-4f7c1c9bbc32 h1:7vZlQmXm2ypWJyRBPeX1Mson/dsReRDw7yIoLQGL/8w= -github.com/hashicorp/hcl-lang v0.0.0-20241209140757-4f7c1c9bbc32/go.mod h1:IZQIEGz+2WgWElRh8Tkc8gxT9AzPXMrRBjn2+iBkqdc= +github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b h1:JWLbh10Hji/SYrBGwaWmvmqvbbOxQzuFZ0CplYCwCM4= +github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b/go.mod h1:7aFvdIfHocBadjQ6j5RbxV0rSEasCPj0RTj/ujGCmi8= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= @@ -237,8 +237,8 @@ github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4 github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= -github.com/hashicorp/terraform-schema v0.0.0-20241212141216-b4693e6bc465 h1:W1KHI7/MaoHT7wKFLj6eqDX0rQYoHky/TqJUPOj9s1o= -github.com/hashicorp/terraform-schema v0.0.0-20241212141216-b4693e6bc465/go.mod h1:3vDqHlpaMuTeBXSC4LWDM/m2QdEe9DmC90IgyuhdgZw= +github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c h1:g/Y0BUI5Gk1hgMWcI5PpeXtvvmzvQruW6az0yPhFFKk= +github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c/go.mod h1:+fQEDxf+c6PnG7/3ZF26K69zWLnIp/uTmsMffCsuw6o= 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/modules/events.go b/internal/features/modules/events.go index fd40360d..ba325f88 100644 --- a/internal/features/modules/events.go +++ b/internal/features/modules/events.go @@ -360,6 +360,20 @@ func (f *ModulesFeature) decodeModule(ctx context.Context, dir document.DirHandl } } + woAttributesId, err := f.stateStore.JobStore.EnqueueJob(ctx, job.Job{ + Dir: dir, + Func: func(ctx context.Context) error { + return jobs.DecodeWriteOnlyAttributes(ctx, f.Store, f.rootFeature, path) + }, + Type: op.OpTypeDecodeWriteOnlyAttributes.String(), + DependsOn: append(modCalls, eSchemaId), + IgnoreState: ignoreState, + }) + if err != nil { + return deferIds, err + } + deferIds = append(deferIds, woAttributesId) + return deferIds, nil }, }) diff --git a/internal/features/modules/jobs/write_only_attributes.go b/internal/features/modules/jobs/write_only_attributes.go new file mode 100644 index 00000000..97e01e02 --- /dev/null +++ b/internal/features/modules/jobs/write_only_attributes.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package jobs + +import ( + "context" + + "github.com/hashicorp/hcl-lang/decoder" + "github.com/hashicorp/hcl-lang/lang" + idecoder "github.com/hashicorp/terraform-ls/internal/decoder" + "github.com/hashicorp/terraform-ls/internal/document" + fdecoder "github.com/hashicorp/terraform-ls/internal/features/modules/decoder" + "github.com/hashicorp/terraform-ls/internal/features/modules/state" + "github.com/hashicorp/terraform-ls/internal/job" + ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + op "github.com/hashicorp/terraform-ls/internal/terraform/module/operation" + tfaddr "github.com/hashicorp/terraform-registry-address" + tfschema "github.com/hashicorp/terraform-schema/schema" +) + +// DecodeWriteOnlyAttributes collects usages of write only attributes, +// using previously parsed AST (via [ParseModuleConfiguration]), +// core schema of appropriate version (as obtained via [GetTerraformVersion]) +// and provider schemas ([PreloadEmbeddedSchema] or [ObtainSchema]). +func DecodeWriteOnlyAttributes(ctx context.Context, modStore *state.ModuleStore, rootFeature fdecoder.RootReader, modPath string) error { + mod, err := modStore.ModuleRecordByPath(modPath) + if err != nil { + return err + } + + // TODO: Avoid collection if upstream jobs reported no changes + + // Avoid collection if it is already in progress or already done + if mod.WriteOnlyAttributesState != op.OpStateUnknown && !job.IgnoreState(ctx) { + return job.StateNotChangedErr{Dir: document.DirHandleFromPath(modPath)} + } + + err = modStore.SetWriteOnlyAttributesState(modPath, op.OpStateLoading) + if err != nil { + return err + } + + d := decoder.NewDecoder(&fdecoder.PathReader{ + StateReader: modStore, + RootReader: rootFeature, + }) + d.SetContext(idecoder.DecoderContext(ctx)) + + pd, err := d.Path(lang.Path{ + Path: modPath, + LanguageID: ilsp.Terraform.String(), + }) + if err != nil { + return err + } + + // input list of write only attributes + woAttrs, rErr := pd.CollectWriteOnlyAttributes() + + if rErr != nil { + return rErr + } + + findProviderAddr := func(resourceName string) *tfaddr.Provider { + for localRef, addr := range mod.Meta.ProviderReferences { + if tfschema.TypeBelongsToProvider(resourceName, localRef) { + return &addr + } + } + return nil + } + + // output counts of write only attributes aggregated by provider, resource and attribute + woAttrsMap := make(state.WriteOnlyAttributes) + + // count usages and resolve provider + for _, attr := range woAttrs { + providerAddr := findProviderAddr(attr.Resource) + if providerAddr == nil { + continue + } + + if _, ok := woAttrsMap[*providerAddr]; !ok { + woAttrsMap[*providerAddr] = make(map[state.ResourceName]map[state.AttributeName]int) + } + + if _, ok := woAttrsMap[*providerAddr][attr.Resource]; !ok { + woAttrsMap[*providerAddr][attr.Resource] = make(map[state.AttributeName]int) + } + + woAttrsMap[*providerAddr][attr.Resource][attr.Name]++ + } + + sErr := modStore.UpdateWriteOnlyAttributes(modPath, woAttrsMap, rErr) + if sErr != nil { + return sErr + } + + return rErr +} diff --git a/internal/features/modules/modules_feature.go b/internal/features/modules/modules_feature.go index bf42143e..f905a266 100644 --- a/internal/features/modules/modules_feature.go +++ b/internal/features/modules/modules_feature.go @@ -266,6 +266,18 @@ func (f *ModulesFeature) Telemetry(path string) map[string]interface{} { properties["providerRequirements"] = reqs } + if len(mod.WriteOnlyAttributes) > 0 { + woAttrs := make(map[string]map[string]map[string]int) + + for pAddr, stats := range mod.WriteOnlyAttributes { + if telemetry.IsPublicProvider(pAddr) { + woAttrs[pAddr.String()] = stats + } + } + + properties["writeOnlyAttributes"] = woAttrs + } + modId, err := f.Store.GetModuleID(mod.Path()) if err != nil { return properties diff --git a/internal/features/modules/state/module_record.go b/internal/features/modules/state/module_record.go index 70276820..e8138c12 100644 --- a/internal/features/modules/state/module_record.go +++ b/internal/features/modules/state/module_record.go @@ -36,6 +36,10 @@ type ModuleRecord struct { MetaErr error MetaState op.OpState + WriteOnlyAttributes WriteOnlyAttributes + WriteOnlyAttributesErr error + WriteOnlyAttributesState op.OpState + ModuleDiagnostics ast.SourceModDiags ModuleDiagnosticsState globalAst.DiagnosticSourceState } @@ -63,6 +67,10 @@ func (m *ModuleRecord) Copy() *ModuleRecord { MetaErr: m.MetaErr, MetaState: m.MetaState, + WriteOnlyAttributes: m.WriteOnlyAttributes, + WriteOnlyAttributesErr: m.WriteOnlyAttributesErr, + WriteOnlyAttributesState: m.WriteOnlyAttributesState, + ModuleDiagnosticsState: m.ModuleDiagnosticsState.Copy(), } @@ -101,6 +109,7 @@ func newModule(modPath string) *ModuleRecord { RefOriginsState: op.OpStateUnknown, RefTargetsState: op.OpStateUnknown, MetaState: op.OpStateUnknown, + WriteOnlyAttributesState: op.OpStateUnknown, ModuleDiagnosticsState: globalAst.DiagnosticSourceState{ globalAst.HCLParsingSource: op.OpStateUnknown, globalAst.SchemaValidationSource: op.OpStateUnknown, diff --git a/internal/features/modules/state/module_store.go b/internal/features/modules/state/module_store.go index 976495b9..0fd9ad4b 100644 --- a/internal/features/modules/state/module_store.go +++ b/internal/features/modules/state/module_store.go @@ -566,6 +566,49 @@ func (s *ModuleStore) UpdateReferenceOrigins(path string, origins reference.Orig return nil } +func (s *ModuleStore) SetWriteOnlyAttributesState(path string, state op.OpState) error { + txn := s.db.Txn(true) + defer txn.Abort() + + mod, err := moduleCopyByPath(txn, path) + if err != nil { + return err + } + + mod.WriteOnlyAttributesState = state + err = txn.Insert(s.tableName, mod) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (s *ModuleStore) UpdateWriteOnlyAttributes(path string, woAttrs WriteOnlyAttributes, woAttrsErr error) error { + txn := s.db.Txn(true) + txn.Defer(func() { + s.SetWriteOnlyAttributesState(path, op.OpStateLoaded) + }) + defer txn.Abort() + + mod, err := moduleCopyByPath(txn, path) + if err != nil { + return err + } + + mod.WriteOnlyAttributes = woAttrs + mod.WriteOnlyAttributesErr = woAttrsErr + + err = txn.Insert(s.tableName, mod) + if err != nil { + return err + } + + txn.Commit() + return nil +} + func (s *ModuleStore) RegistryModuleMeta(addr tfaddr.Module, cons version.Constraints) (*registry.ModuleData, error) { return s.registryModuleStore.RegistryModuleMeta(addr, cons) } diff --git a/internal/features/modules/state/write_only_attributes.go b/internal/features/modules/state/write_only_attributes.go new file mode 100644 index 00000000..4587148d --- /dev/null +++ b/internal/features/modules/state/write_only_attributes.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package state + +import ( + tfaddr "github.com/hashicorp/terraform-registry-address" +) + +type ResourceName = string +type AttributeName = string + +type WriteOnlyAttributes map[tfaddr.Provider]map[ResourceName]map[AttributeName]int diff --git a/internal/terraform/module/operation/op_type_string.go b/internal/terraform/module/operation/op_type_string.go index a78cdde7..6c1964ce 100644 --- a/internal/terraform/module/operation/op_type_string.go +++ b/internal/terraform/module/operation/op_type_string.go @@ -37,12 +37,13 @@ func _() { _ = x[OpTypeLoadTestMetadata-26] _ = x[OpTypeDecodeTestReferenceTargets-27] _ = x[OpTypeDecodeTestReferenceOrigins-28] - _ = x[OpTypeSchemaTestValidation-29] + _ = x[OpTypeDecodeWriteOnlyAttributes-29] + _ = x[OpTypeSchemaTestValidation-30] } -const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeGetInstalledTerraformVersionOpTypeObtainSchemaOpTypeParseModuleConfigurationOpTypeParseVariablesOpTypeParseModuleManifestOpTypeParseTerraformSourcesOpTypeLoadModuleMetadataOpTypeDecodeReferenceTargetsOpTypeDecodeReferenceOriginsOpTypeDecodeVarsReferencesOpTypeGetModuleDataFromRegistryOpTypeParseProviderVersionsOpTypePreloadEmbeddedSchemaOpTypeStacksPreloadEmbeddedSchemaOpTypeSchemaModuleValidationOpTypeSchemaStackValidationOpTypeSchemaVarsValidationOpTypeReferenceValidationOpTypeReferenceStackValidationOpTypeTerraformValidateOpTypeParseStackConfigurationOpTypeLoadStackMetadataOpTypeLoadStackRequiredTerraformVersionOpTypeParseTestConfigurationOpTypeLoadTestMetadataOpTypeDecodeTestReferenceTargetsOpTypeDecodeTestReferenceOriginsOpTypeSchemaTestValidation" +const _OpType_name = "OpTypeUnknownOpTypeGetTerraformVersionOpTypeGetInstalledTerraformVersionOpTypeObtainSchemaOpTypeParseModuleConfigurationOpTypeParseVariablesOpTypeParseModuleManifestOpTypeParseTerraformSourcesOpTypeLoadModuleMetadataOpTypeDecodeReferenceTargetsOpTypeDecodeReferenceOriginsOpTypeDecodeVarsReferencesOpTypeGetModuleDataFromRegistryOpTypeParseProviderVersionsOpTypePreloadEmbeddedSchemaOpTypeStacksPreloadEmbeddedSchemaOpTypeSchemaModuleValidationOpTypeSchemaStackValidationOpTypeSchemaVarsValidationOpTypeReferenceValidationOpTypeReferenceStackValidationOpTypeTerraformValidateOpTypeParseStackConfigurationOpTypeLoadStackMetadataOpTypeLoadStackRequiredTerraformVersionOpTypeParseTestConfigurationOpTypeLoadTestMetadataOpTypeDecodeTestReferenceTargetsOpTypeDecodeTestReferenceOriginsOpTypeDecodeWriteOnlyAttributesOpTypeSchemaTestValidation" -var _OpType_index = [...]uint16{0, 13, 38, 72, 90, 120, 140, 165, 192, 216, 244, 272, 298, 329, 356, 383, 416, 444, 471, 497, 522, 552, 575, 604, 627, 666, 694, 716, 748, 780, 806} +var _OpType_index = [...]uint16{0, 13, 38, 72, 90, 120, 140, 165, 192, 216, 244, 272, 298, 329, 356, 383, 416, 444, 471, 497, 522, 552, 575, 604, 627, 666, 694, 716, 748, 780, 811, 837} func (i OpType) String() string { if i >= OpType(len(_OpType_index)-1) { diff --git a/internal/terraform/module/operation/operation.go b/internal/terraform/module/operation/operation.go index be574440..85f1dcdc 100644 --- a/internal/terraform/module/operation/operation.go +++ b/internal/terraform/module/operation/operation.go @@ -46,5 +46,6 @@ const ( OpTypeLoadTestMetadata OpTypeDecodeTestReferenceTargets OpTypeDecodeTestReferenceOrigins + OpTypeDecodeWriteOnlyAttributes OpTypeSchemaTestValidation )