diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de60e2d7..b8b5418a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ENHANCEMENTS: - `azapi` resources/data sources: Update embedded schema to the latest version. - Support specifying the provider version used in the migration in the `terraform` block. +- `azapi_resource`, `azapi_update_resource` resources: Support `sensitive_body` field, which is used to specify the write-only properties in the request body. BUG FIXES: - Fix a bug that resource group's api-version `2024-07-01` is disabled in the provider. diff --git a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json index 88ce78437..fc32ba9c7 100644 --- a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json +++ b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json @@ -224,6 +224,36 @@ "command": "editor.action.triggerSuggest" } }, + { + "label": "sensitive_body", + "labelDetails": {}, + "kind": 10, + "detail": "sensitive_body (Optional)", + "documentation": { + "kind": "markdown", + "value": "Type: `dynamic` \nA dynamic attribute that contains the write-only properties of the request body. This will be merge-patched to the body to construct the actual request body.\n" + }, + "sortText": "0006", + "insertTextFormat": 2, + "insertTextMode": 2, + "textEdit": { + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 2 + } + }, + "newText": "sensitive_body = $0" + }, + "command": { + "title": "Suggest", + "command": "editor.action.triggerSuggest" + } + }, { "label": "tags", "labelDetails": {}, @@ -233,7 +263,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of tags which should be assigned to the azure resource.\n" }, - "sortText": "0006", + "sortText": "0007", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -263,7 +293,7 @@ "kind": "markdown", "value": "Type: `list or map` \nThe attribute can accept either a list or a map of path that needs to be exported from response body.\n" }, - "sortText": "0007", + "sortText": "0008", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -293,7 +323,7 @@ "kind": "markdown", "value": "Type: `dynamic` \nWill trigger a replace of the resource when the value changes and is not `null`.\n" }, - "sortText": "0008", + "sortText": "0009", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -323,7 +353,7 @@ "kind": "markdown", "value": "Type: `block` \nConfiguration block for custom retry policy.\n" }, - "sortText": "0009", + "sortText": "0010", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -353,7 +383,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether enabled the validation on `type` and `body` with embedded schema. Defaults to `true`.\n" }, - "sortText": "0010", + "sortText": "0011", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -383,7 +413,7 @@ "kind": "markdown", "value": "Type: `list` \nA list of ARM resource IDs which are used to avoid create/modify/delete azapi resources at the same time.\n" }, - "sortText": "0011", + "sortText": "0012", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -413,7 +443,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether ignore incorrect casing returned in `body` to suppress plan-diff. Defaults to `false`.\n" }, - "sortText": "0012", + "sortText": "0013", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -443,7 +473,7 @@ "kind": "markdown", "value": "Type: `bool` \nWhether ignore not returned properties like credentials in `body` to suppress plan-diff. Defaults to `false`.\n" }, - "sortText": "0013", + "sortText": "0014", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -473,7 +503,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of headers which should be sent with the create request.\n" }, - "sortText": "0014", + "sortText": "0015", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -503,7 +533,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of headers which should be sent with the update request.\n" }, - "sortText": "0015", + "sortText": "0016", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -533,7 +563,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of headers which should be sent with the read request.\n" }, - "sortText": "0016", + "sortText": "0017", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -563,7 +593,7 @@ "kind": "markdown", "value": "Type: `map` \nA mapping of headers which should be sent with the delete request.\n" }, - "sortText": "0017", + "sortText": "0018", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -593,7 +623,7 @@ "kind": "markdown", "value": "Type: `map>` \nA mapping of query parameters which should be sent with the create request.\n" }, - "sortText": "0018", + "sortText": "0019", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -623,7 +653,7 @@ "kind": "markdown", "value": "Type: `map>` \nA mapping of query parameters which should be sent with the update request.\n" }, - "sortText": "0019", + "sortText": "0020", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -653,7 +683,7 @@ "kind": "markdown", "value": "Type: `map>` \nA mapping of query parameters which should be sent with the read request.\n" }, - "sortText": "0020", + "sortText": "0021", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { @@ -683,7 +713,7 @@ "kind": "markdown", "value": "Type: `map>` \nA mapping of query parameters which should be sent with the delete request.\n" }, - "sortText": "0021", + "sortText": "0022", "insertTextFormat": 2, "insertTextMode": 2, "textEdit": { diff --git a/internal/langserver/handlers/testdata/TestValidation_sensitiveBody/main.tf b/internal/langserver/handlers/testdata/TestValidation_sensitiveBody/main.tf new file mode 100644 index 000000000..4a23907b6 --- /dev/null +++ b/internal/langserver/handlers/testdata/TestValidation_sensitiveBody/main.tf @@ -0,0 +1,86 @@ +resource "azapi_resource" "resourceGroup" { + type = "Microsoft.Resources/resourceGroups@2021-04-01" + name = "henglu415" + location = "westus" + body = {} # Resource group does not require additional properties in the body + lifecycle { + ignore_changes = [ + tags, + ] + } +} + +resource "azapi_resource" "storageAccount" { + type = "Microsoft.Storage/storageAccounts@2021-02-01" + parent_id = azapi_resource.resourceGroup.id + name = "henglu415storage" + location = "westus" + body = { + kind = "StorageV2" + sku = { + name = "Premium_LRS" + } + } +} + +ephemeral "azapi_resource_action" "listKeys" { + type = "Microsoft.Storage/storageAccounts@2024-01-01" + resource_id = azapi_resource.storageAccount.id + action = "listKeys" + response_export_values = { + primary_access_key = "keys[0].value" + } +} + + +resource "azapi_resource" "workspace" { + type = "Microsoft.OperationalInsights/workspaces@2023-09-01" + parent_id = azapi_resource.resourceGroup.id + name = "henglu415workspace" + location = "westus" + body = { + properties = { + features = { + disableLocalAuth = false + enableLogAccessUsingOnlyResourcePermissions = true + } + publicNetworkAccessForIngestion = "Enabled" + publicNetworkAccessForQuery = "Enabled" + retentionInDays = 30 + sku = { + name = "PerGB2018" + } + workspaceCapping = { + dailyQuotaGb = -1 + } + } + } + +} + +resource "azapi_resource" "storageInsightConfig" { + type = "Microsoft.OperationalInsights/workspaces/storageInsightConfigs@2023-09-01" + parent_id = azapi_resource.workspace.id + name = "storageInsightConfig" + location = "westus" + identity { + type = "SystemAssigned" + identity_ids = [] + } + + body = { + properties = { + storageAccount = { + id = azapi_resource.storageAccount.id + } + } + } + + sensitive_body = { + properties = { + storageAccount = { + key = ephemeral.azapi_resource_action.listKeys.output.primary_access_key + } + } + } +} diff --git a/internal/langserver/handlers/tfschema/init.go b/internal/langserver/handlers/tfschema/init.go index cb2041500..dd76a279d 100644 --- a/internal/langserver/handlers/tfschema/init.go +++ b/internal/langserver/handlers/tfschema/init.go @@ -125,6 +125,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "sensitive_body", + Modifier: "Optional", + Type: "dynamic", + CompletionNewText: "sensitive_body = $0", + Description: "A dynamic attribute that contains the write-only properties of the request body. This will be merge-patched to the body to construct the actual request body.", + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: bodyCandidates, + }, + { Name: "tags", Modifier: "Optional", @@ -297,6 +307,16 @@ func init() { GenericCandidatesFunc: bodyCandidates, }, + { + Name: "sensitive_body", + Modifier: "Optional", + Type: "dynamic", + CompletionNewText: "sensitive_body = $0", + Description: "A dynamic attribute that contains the write-only properties of the request body. This will be merge-patched to the body to construct the actual request body.", + ValueCandidatesFunc: FixedValueCandidatesFunc([]lsp.CompletionItem{dynamicPlaceholderCandidate()}), + GenericCandidatesFunc: bodyCandidates, + }, + { Name: "response_export_values", Modifier: "Optional", diff --git a/internal/langserver/handlers/validate/validate.go b/internal/langserver/handlers/validate/validate.go index 569fbd6ed..94382a7c4 100644 --- a/internal/langserver/handlers/validate/validate.go +++ b/internal/langserver/handlers/validate/validate.go @@ -75,6 +75,11 @@ func ValidateBlock(src []byte, block *hclsyntax.Block) hcl.Diagnostics { hclNode = parser.BuildHclNode(tokens) } } + if sensitiveBodyAttribute := parser.AttributeWithName(block, "sensitive_body"); sensitiveBodyAttribute != nil { + tokens, _ := hclsyntax.LexExpression(src[sensitiveBodyAttribute.Expr.Range().Start.Byte:sensitiveBodyAttribute.Expr.Range().End.Byte], "", sensitiveBodyAttribute.Expr.Range().Start) + sensitiveHclNode := parser.BuildHclNode(tokens) + hclNode = parser.CombineHclNodes(hclNode, sensitiveHclNode) + } if attribute == nil || hclNode == nil { return nil } diff --git a/internal/langserver/handlers/validate/validate_test.go b/internal/langserver/handlers/validate/validate_test.go index 348bf7354..40784e73e 100644 --- a/internal/langserver/handlers/validate/validate_test.go +++ b/internal/langserver/handlers/validate/validate_test.go @@ -153,3 +153,15 @@ func TestValidation_payload_missingRequiredPropertyInArrayItem(t *testing.T) { } } } + +func TestValidation_sensitiveBody(t *testing.T) { + config, err := os.ReadFile(fmt.Sprintf("../testdata/%s/main.tf", t.Name())) + if err != nil { + t.Fatal(err) + } + + _, diag := ValidateFile(config, "main.tf") + if len(diag) != 0 { + t.Errorf("expect no diagnostics, but got %v", diag) + } +} diff --git a/internal/parser/hcl_node.go b/internal/parser/hcl_node.go index 27b42a8cf..6d63167f9 100644 --- a/internal/parser/hcl_node.go +++ b/internal/parser/hcl_node.go @@ -416,3 +416,23 @@ func fixEmptyValueRange(hclNode *HclNode) { } } } + +func CombineHclNodes(a, b *HclNode) *HclNode { + if a == nil && b == nil { + return nil + } + if a == nil { + return b + } + if b == nil { + return a + } + for k, v := range b.Children { + if _, ok := a.Children[k]; !ok { + a.Children[k] = v + } else { + a.Children[k] = CombineHclNodes(a.Children[k], v) + } + } + return a +}