Skip to content

Commit

Permalink
feat: Report usage of write-only attributes for public providers (#1926)
Browse files Browse the repository at this point in the history
* feat: Report usage of write-only attributes (only public providers)
* Bump terraform-schema to `3c49914`
* Bump hcl-lang to `66cdc97`
* chore: add changie entry
  • Loading branch information
ansgarm authored Jan 17, 2025
1 parent fde519d commit b9f4ebe
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20250117-164515.yaml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
14 changes: 14 additions & 0 deletions internal/features/modules/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
})
Expand Down
101 changes: 101 additions & 0 deletions internal/features/modules/jobs/write_only_attributes.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 12 additions & 0 deletions internal/features/modules/modules_feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions internal/features/modules/state/module_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(),
}

Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions internal/features/modules/state/module_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
13 changes: 13 additions & 0 deletions internal/features/modules/state/write_only_attributes.go
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions internal/terraform/module/operation/op_type_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/terraform/module/operation/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ const (
OpTypeLoadTestMetadata
OpTypeDecodeTestReferenceTargets
OpTypeDecodeTestReferenceOrigins
OpTypeDecodeWriteOnlyAttributes
OpTypeSchemaTestValidation
)

0 comments on commit b9f4ebe

Please sign in to comment.