Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17
github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.29.9
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.5
github.com/evanphx/json-patch v0.5.2
github.com/google/go-cmp v0.7.0
github.com/google/go-github/v72 v72.0.0
github.com/hashicorp/aws-cloudformation-resource-schema-sdk-go v0.23.0
Expand Down Expand Up @@ -60,7 +61,6 @@ require (
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down Expand Up @@ -95,6 +95,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
Expand Down
2 changes: 0 additions & 2 deletions internal/aws/ecs/service_resource_gen.go

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

7 changes: 7 additions & 0 deletions internal/provider/all_schemas.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -2256,6 +2256,13 @@ resource_schema "aws_ecs_primary_task_set" {

resource_schema "aws_ecs_service" {
cloudformation_type_name = "AWS::ECS::Service"

schema_patches {
operation {
action = "remove"
json_path = "/properties/PlatformVersion/default"
}
}
}

resource_schema "aws_ecs_task_definition" {
Expand Down
41 changes: 32 additions & 9 deletions internal/provider/generators/schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,25 @@
}

type ResourceSchema struct {
CloudFormationSchemaPath string `hcl:"cloudformation_schema_path,optional"`
CloudFormationTypeName string `hcl:"cloudformation_type_name"`
ResourceTypeName string `hcl:"resource_type_name,label"`
SuppressionReason string `hcl:"suppression_reason,optional"`
SuppressPluralDataSourceGeneration bool `hcl:"suppress_plural_data_source_generation,optional"`
SuppressResourceGeneration bool `hcl:"suppress_resource_generation,optional"`
SuppressSingularDataSourceGeneration bool `hcl:"suppress_singular_data_source_generation,optional"`
CloudFormationSchemaPath string `hcl:"cloudformation_schema_path,optional"`
CloudFormationTypeName string `hcl:"cloudformation_type_name"`
ResourceTypeName string `hcl:"resource_type_name,label"`
SuppressionReason string `hcl:"suppression_reason,optional"`
SuppressPluralDataSourceGeneration bool `hcl:"suppress_plural_data_source_generation,optional"`
SuppressResourceGeneration bool `hcl:"suppress_resource_generation,optional"`
SuppressSingularDataSourceGeneration bool `hcl:"suppress_singular_data_source_generation,optional"`
SchemaPatches *SchemaPatches `hcl:"schema_patches,block"`
}

// SchemaPatches contains patch operations to apply to the CloudFormation schema.
type SchemaPatches struct {
Operations []PatchOperation `hcl:"operation,block"`
}

// PatchOperation defines a single patch operation to apply to the schema.
type PatchOperation struct {
Action string `hcl:"action"` // RFC 6902 operation: "remove"
JSONPath string `hcl:"json_path"` // RFC 6902 JSON Pointer path, e.g., "/properties/PlatformVersion/default"
}

var (
Expand Down Expand Up @@ -362,13 +374,24 @@
return "", "", fmt.Errorf("describing CloudFormation type: %w", err)
}

schema, err := cfschema.Sanitize(aws.ToString(output.Schema))
schemaStr, err := cfschema.Sanitize(aws.ToString(output.Schema))

if err != nil {
return "", "", fmt.Errorf("sanitizing schema: %w", err)
}

err = os.WriteFile(dst, []byte(schema), 0644) //nolint:mnd
// Apply schema patches if configured
if schema.SchemaPatches != nil && len(schema.SchemaPatches.Operations) > 0 {
d.infof("applying %d schema patch(es) to %s", len(schema.SchemaPatches.Operations), schema.CloudFormationTypeName)
patchedSchema, patchErr := ApplySchemaPatches(schemaStr, schema.SchemaPatches.Operations)
if patchErr != nil {
d.ui.Warn(fmt.Sprintf("failed to apply schema patches for %s: %s (continuing with unpatched schema)", schema.CloudFormationTypeName, patchErr))
} else {
schemaStr = patchedSchema
}
}

err = os.WriteFile(dst, []byte(schemaStr), 0644)

Check failure on line 394 in internal/provider/generators/schema/main.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Magic number: 0644, in <argument> detected (mnd)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
err = os.WriteFile(dst, []byte(schemaStr), 0644)
err = os.WriteFile(dst, []byte(schemaStr), 0644) //nolint:mnd

Const would be better if there are other uses in schema but if not suppression is fine.


if err != nil {
return "", "", fmt.Errorf("writing schema to %q: %w", dst, err)
Expand Down
59 changes: 59 additions & 0 deletions internal/provider/generators/schema/patches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0

package main

import (
"encoding/json"
"fmt"

jsonpatch "github.com/evanphx/json-patch"
)

// ApplySchemaPatches applies a list of patch operations to a JSON schema string.
// It uses RFC 6902 JSON Patch format to apply the patches.
// Currently only supports "remove" action (default).
// If no operations are provided, it returns the original schema unchanged.
func ApplySchemaPatches(schemaJSON string, operations []PatchOperation) (string, error) {
if len(operations) == 0 {
return schemaJSON, nil
}

// Create JSON Patch document
patchOps := make([]map[string]interface{}, 0, len(operations))
for _, op := range operations {
if op.Action == "" {
return "", fmt.Errorf("action is required for path %q", op.JSONPath)
}

// Currently only "remove" is supported
if op.Action != "remove" {
return "", fmt.Errorf("unsupported action %q for path %q: only \"remove\" is currently supported", op.Action, op.JSONPath)
}

patchOps = append(patchOps, map[string]interface{}{
"op": op.Action,
"path": op.JSONPath,
})
}

// Marshal patch operations to JSON
patchBytes, err := json.Marshal(patchOps)
if err != nil {
return "", fmt.Errorf("failed to marshal patch operations: %w", err)
}

// Decode and apply the patch
patch, err := jsonpatch.DecodePatch(patchBytes)
if err != nil {
return "", fmt.Errorf("failed to decode patch: %w", err)
}

// Apply patch to schema
patchedBytes, err := patch.Apply([]byte(schemaJSON))
if err != nil {
return "", fmt.Errorf("failed to apply patch: %w", err)
}

return string(patchedBytes), nil
}
215 changes: 215 additions & 0 deletions internal/provider/generators/schema/patches_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0

package main

import (
"encoding/json"
"testing"
)

func TestApplySchemaPatches(t *testing.T) {
tests := []struct {
name string
schemaJSON string
operations []PatchOperation
wantJSON string
wantErr bool
}{
{
name: "remove existing property default",
schemaJSON: `{
"properties": {
"PlatformVersion": {
"type": "string",
"default": "LATEST"
}
}
}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/PlatformVersion/default"},
},
wantJSON: `{
"properties": {
"PlatformVersion": {
"type": "string"
}
}
}`,
wantErr: false,
},
{
name: "remove nested property",
schemaJSON: `{
"properties": {
"Config": {
"type": "object",
"properties": {
"Settings": {
"default": "value",
"type": "string"
}
}
}
}
}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/Config/properties/Settings/default"},
},
wantJSON: `{
"properties": {
"Config": {
"type": "object",
"properties": {
"Settings": {
"type": "string"
}
}
}
}
}`,
wantErr: false,
},
{
name: "multiple remove operations",
schemaJSON: `{
"properties": {
"Prop1": {
"type": "string",
"default": "value1"
},
"Prop2": {
"type": "number",
"default": 42
}
}
}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/Prop1/default"},
{Action: "remove", JSONPath: "/properties/Prop2/default"},
},
wantJSON: `{
"properties": {
"Prop1": {
"type": "string"
},
"Prop2": {
"type": "number"
}
}
}`,
wantErr: false,
},
{
name: "no operations returns original",
schemaJSON: `{"properties": {"Foo": "bar"}}`,
operations: []PatchOperation{},
wantJSON: `{"properties": {"Foo": "bar"}}`,
wantErr: false,
},
{
name: "nil operations returns original",
schemaJSON: `{"properties": {"Foo": "bar"}}`,
operations: nil,
wantJSON: `{"properties": {"Foo": "bar"}}`,
wantErr: false,
},
{
name: "remove non-existent path returns error",
schemaJSON: `{"properties": {}}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/NonExistent/default"},
},
wantJSON: "",
wantErr: true,
},
{
name: "invalid JSON input returns error",
schemaJSON: `{invalid json`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/Foo"},
},
wantJSON: "",
wantErr: true,
},
{
name: "remove entire property",
schemaJSON: `{
"properties": {
"Keep": {"type": "string"},
"Remove": {"type": "string"}
}
}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/properties/Remove"},
},
wantJSON: `{
"properties": {
"Keep": {"type": "string"}
}
}`,
wantErr: false,
},
{
name: "remove array element",
schemaJSON: `{
"required": ["a", "b", "c"]
}`,
operations: []PatchOperation{
{Action: "remove", JSONPath: "/required/1"},
},
wantJSON: `{
"required": ["a", "c"]
}`,
wantErr: false,
},
{
name: "missing action returns error",
schemaJSON: `{"properties": {"Foo": "bar"}}`,
operations: []PatchOperation{
{JSONPath: "/properties/Foo"},
},
wantJSON: "",
wantErr: true,
},
{
name: "unsupported action returns error",
schemaJSON: `{"properties": {}}`,
operations: []PatchOperation{
{Action: "add", JSONPath: "/properties/Foo"},
},
wantJSON: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplySchemaPatches(tt.schemaJSON, tt.operations)
if (err != nil) != tt.wantErr {
t.Errorf("ApplySchemaPatches() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}

// Compare JSON semantically (ignore formatting differences)
var gotJSON, wantJSON interface{}
if err := json.Unmarshal([]byte(got), &gotJSON); err != nil {
t.Errorf("ApplySchemaPatches() returned invalid JSON: %v", err)
return
}
if err := json.Unmarshal([]byte(tt.wantJSON), &wantJSON); err != nil {
t.Errorf("Test case has invalid wantJSON: %v", err)
return
}

gotBytes, _ := json.Marshal(gotJSON)
wantBytes, _ := json.Marshal(wantJSON)
if string(gotBytes) != string(wantBytes) {
t.Errorf("ApplySchemaPatches() = %s, want %s", string(gotBytes), string(wantBytes))
}
})
}
}
2 changes: 1 addition & 1 deletion internal/provider/schemas.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0

//go:generate go run generators/schema/main.go -config all_schemas.hcl -generated-code-root .. -import-path-root github.com/hashicorp/terraform-provider-awscc/internal -- resources.go singular_data_sources.go plural_data_sources.go import_examples_gen.json
//go:generate go run generators/schema/main.go generators/schema/patches.go -config all_schemas.hcl -generated-code-root .. -import-path-root github.com/hashicorp/terraform-provider-awscc/internal -- resources.go singular_data_sources.go plural_data_sources.go import_examples_gen.json

package provider
Loading
Loading