Skip to content
Merged
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
36 changes: 36 additions & 0 deletions src/schema/transformers/ResourceEdgeCaseTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ResourceSchema } from '../ResourceSchema';
import type { ResourceTemplateTransformer } from './ResourceTemplateTransformer';

type EdgeCaseHandler = (resourceProperties: Record<string, unknown>) => void;

// WAFv2 IPSet Description pattern requires non-whitespace start/end chars
const removeEmptyOrWhitespaceDescription: EdgeCaseHandler = (resourceProperties) => {
if (typeof resourceProperties.Description !== 'string') {
return;
}
if (resourceProperties.Description.trim() === '') {
delete resourceProperties.Description;
}
};

/**
* Transformer that handles specific edge cases not covered by schema-based transformers.
* Add handlers here for resource-specific issues that cause deployment failures.
*/
export class ResourceEdgeCaseTransformer implements ResourceTemplateTransformer {
private readonly handlers: ReadonlyMap<string, EdgeCaseHandler[]> = new Map([
// WAFv2 IPSet: Description pattern requires non-whitespace start/end chars
['AWS::WAFv2::IPSet', [removeEmptyOrWhitespaceDescription]],
]);

public transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void {
const handlers = this.handlers.get(schema.typeName);
if (!handlers) {
return;
}

for (const handler of handlers) {
handler(resourceProperties);
}
}
}
3 changes: 3 additions & 0 deletions src/schema/transformers/TransformersUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RemoveReadonlyPropertiesTransformer } from './RemoveReadonlyPropertiesT
import { RemoveRequiredXorPropertiesTransformer } from './RemoveRequiredXorPropertiesTransformer';
import { RemoveSystemTagsTransformer } from './RemoveSystemTagsTransformer';
import { ReplacePrimaryIdentifierTransformer } from './ReplacePrimaryIdentifierTransformer';
import { ResourceEdgeCaseTransformer } from './ResourceEdgeCaseTransformer';
import type { ResourceTemplateTransformer } from './ResourceTemplateTransformer';

export class TransformersUtil {
Expand All @@ -16,6 +17,7 @@ export class TransformersUtil {
new RemoveSystemTagsTransformer(),
new RemoveRequiredXorPropertiesTransformer(),
new AddWriteOnlyRequiredPropertiesTransformer(),
new ResourceEdgeCaseTransformer(),
];
} else if (purpose === ResourceStatePurpose.CLONE) {
return [
Expand All @@ -25,6 +27,7 @@ export class TransformersUtil {
new ReplacePrimaryIdentifierTransformer(),
new RemoveRequiredXorPropertiesTransformer(),
new AddWriteOnlyRequiredPropertiesTransformer(),
new ResourceEdgeCaseTransformer(),
];
}
return [];
Expand Down
130 changes: 130 additions & 0 deletions tst/resources/schemas/aws-wafv2-ipset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"typeName" : "AWS::WAFv2::IPSet",
"description" : "Contains a list of IP addresses. This can be either IPV4 or IPV6. The list will be mutually",
"sourceUrl" : "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-wafv2.git",
"definitions" : {
"EntityName" : {
"description" : "Name of the IPSet.",
"type" : "string",
"pattern" : "^[0-9A-Za-z_-]{1,128}$"
},
"EntityDescription" : {
"description" : "Description of the entity.",
"type" : "string",
"pattern" : "^[a-zA-Z0-9=:#@/\\-,.][a-zA-Z0-9+=:#@/\\-,.\\s]+[a-zA-Z0-9+=:#@/\\-,.]{1,256}$"
},
"EntityId" : {
"description" : "Id of the IPSet",
"type" : "string",
"pattern" : "^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$"
},
"Scope" : {
"description" : "Use CLOUDFRONT for CloudFront IPSet, use REGIONAL for Application Load Balancer and API Gateway.",
"type" : "string",
"enum" : [ "CLOUDFRONT", "REGIONAL" ]
},
"IPAddressVersion" : {
"description" : "Type of addresses in the IPSet, use IPV4 for IPV4 IP addresses, IPV6 for IPV6 address.",
"type" : "string",
"enum" : [ "IPV4", "IPV6" ]
},
"IPAddress" : {
"description" : "IP address",
"type" : "string",
"maxLength" : 50,
"minLength" : 1
},
"ResourceArn" : {
"description" : "ARN of the WAF entity.",
"type" : "string"
},
"Tag" : {
"type" : "object",
"properties" : {
"Key" : {
"type" : "string",
"minLength" : 1,
"maxLength" : 128
},
"Value" : {
"type" : "string",
"minLength" : 0,
"maxLength" : 256
}
},
"additionalProperties" : false
}
},
"properties" : {
"Arn" : {
"$ref" : "#/definitions/ResourceArn"
},
"Description" : {
"$ref" : "#/definitions/EntityDescription"
},
"Name" : {
"$ref" : "#/definitions/EntityName"
},
"Id" : {
"$ref" : "#/definitions/EntityId"
},
"Scope" : {
"$ref" : "#/definitions/Scope"
},
"IPAddressVersion" : {
"$ref" : "#/definitions/IPAddressVersion"
},
"Addresses" : {
"description" : "List of IPAddresses.",
"type" : "array",
"items" : {
"$ref" : "#/definitions/IPAddress"
}
},
"Tags" : {
"type" : "array",
"items" : {
"$ref" : "#/definitions/Tag"
},
"minItems" : 1
}
},
"required" : [ "Addresses", "IPAddressVersion", "Scope" ],
"primaryIdentifier" : [ "/properties/Name", "/properties/Id", "/properties/Scope" ],
"createOnlyProperties" : [ "/properties/Name", "/properties/Scope" ],
"readOnlyProperties" : [ "/properties/Arn", "/properties/Id" ],
"additionalProperties" : false,
"tagging" : {
"cloudFormationSystemTags" : true,
"tagOnCreate" : true,
"tagUpdatable" : true,
"taggable" : true,
"tagProperty" : "/properties/Tags",
"permissions" : [ "wafv2:TagResource", "wafv2:UntagResource", "wafv2:ListTagsForResource" ]
},
"handlers" : {
"create" : {
"permissions" : [ "wafv2:CreateIPSet", "wafv2:GetIPSet", "wafv2:ListTagsForResource", "wafv2:TagResource", "wafv2:UntagResource" ]
},
"delete" : {
"permissions" : [ "wafv2:DeleteIPSet", "wafv2:GetIPSet" ]
},
"read" : {
"permissions" : [ "wafv2:GetIPSet", "wafv2:ListTagsForResource" ]
},
"update" : {
"permissions" : [ "wafv2:UpdateIPSet", "wafv2:GetIPSet", "wafv2:ListTagsForResource", "wafv2:TagResource", "wafv2:UntagResource" ]
},
"list" : {
"permissions" : [ "wafv2:listIPSets" ],
"handlerSchema" : {
"properties" : {
"Scope" : {
"$ref" : "resource-schema.json#/properties/Scope"
}
},
"required" : [ "Scope" ]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest';
import { ResourceEdgeCaseTransformer } from '../../../../src/schema/transformers/ResourceEdgeCaseTransformer';
import { combinedSchemas } from '../../../utils/SchemaUtils';

describe('ResourceEdgeCaseTransformer', () => {
const schemas = combinedSchemas();
const transformer = new ResourceEdgeCaseTransformer();

it('should remove empty Description from AWS::WAFv2::IPSet', () => {
const schema = schemas.schemas.get('AWS::WAFv2::IPSet')!;
const resourceProperties = {
Name: 'test-ipset',
Description: '',
Scope: 'REGIONAL',
};

transformer.transform(resourceProperties, schema);

expect(resourceProperties.Description).toBeUndefined();
});

it('should remove whitespace-only Description from AWS::WAFv2::IPSet', () => {
const schema = schemas.schemas.get('AWS::WAFv2::IPSet')!;
const resourceProperties = {
Name: 'test-ipset',
Description: ' ',
Scope: 'REGIONAL',
};

transformer.transform(resourceProperties, schema);

expect(resourceProperties.Description).toBeUndefined();
});

it('should not modify valid Description with leading/trailing whitespace in AWS::WAFv2::IPSet', () => {
const schema = schemas.schemas.get('AWS::WAFv2::IPSet')!;
const resourceProperties = {
Name: 'test-ipset',
Description: ' Valid description ',
Scope: 'REGIONAL',
};

transformer.transform(resourceProperties, schema);

expect(resourceProperties.Description).toBe(' Valid description ');
});

it('should not modify resources without edge case handlers', () => {
const schema = schemas.schemas.get('AWS::S3::Bucket')!;
const resourceProperties = {
BucketName: 'test-bucket',
Description: '',
};

transformer.transform(resourceProperties, schema);

expect(resourceProperties.Description).toBe('');
});
});
6 changes: 6 additions & 0 deletions tst/utils/SchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ export const Schemas = {
return loadSchema('aws-wafv2-webacl.json');
},
},
WAFv2IPSet: {
fileName: 'file://aws-wafv2-ipset.json',
get contents() {
return loadSchema('aws-wafv2-ipset.json');
},
},
};

export const SamSchemaFiles = {
Expand Down