From a823e64804b2ca53485b5bfacd512575424e8342 Mon Sep 17 00:00:00 2001 From: avinashsana Date: Sun, 6 Sep 2026 12:23:02 +0530 Subject: [PATCH] fix(catalog/aws): a Redshift Serverless workgroup lives in its namespace on a diagram -- its subnets and its managed endpoints' subnets are access, not placement, and a Redshift cluster's endpoint authorizations never place it inside the VPCs they admit --- ...ver-lives-in-a-vpc-it-merely-authorizes.md | 19 +++++++++++++++++ .../awsredshiftcluster/v1alpha1/spec.pb.go | 9 +++++--- .../awsredshiftcluster/v1alpha1/spec.proto | 6 +++++- .../v1alpha1/spec.pb.go | 21 +++++++++++++------ .../v1alpha1/spec.proto | 15 +++++++++++-- .../testdata/containment_decisions.txt | 6 +++--- 6 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 _changelog/2026-09/2026-09-06-123000-a-redshift-serverless-workgroup-lives-in-its-namespace-on-a-diagram-and-a-cluster-never-lives-in-a-vpc-it-merely-authorizes.md diff --git a/_changelog/2026-09/2026-09-06-123000-a-redshift-serverless-workgroup-lives-in-its-namespace-on-a-diagram-and-a-cluster-never-lives-in-a-vpc-it-merely-authorizes.md b/_changelog/2026-09/2026-09-06-123000-a-redshift-serverless-workgroup-lives-in-its-namespace-on-a-diagram-and-a-cluster-never-lives-in-a-vpc-it-merely-authorizes.md new file mode 100644 index 0000000000..262530cfaf --- /dev/null +++ b/_changelog/2026-09/2026-09-06-123000-a-redshift-serverless-workgroup-lives-in-its-namespace-on-a-diagram-and-a-cluster-never-lives-in-a-vpc-it-merely-authorizes.md @@ -0,0 +1,19 @@ +# A Redshift Serverless workgroup lives in its namespace on a diagram, and a Redshift cluster never lives in a VPC it merely authorizes + +## What changed + +- **`AwsRedshiftServerlessWorkgroupSpec.subnet_ids` is containment-exempt.** The namespace is the container kind (`container_kind: true`), and the workgroup is the compute plane that attaches to it -- the box AWS's own model draws around its workgroups. Until now the workgroup's subnets were also placement, so a workgroup referencing its namespace and three subnets named two rooms that are not nested in each other, and the diagram had no honest way to choose (the platform's resolver fell back to topological order, which can land the workgroup in a single subnet). Now the namespace places the workgroup and the subnets are the network it reaches into by lines -- the verdict an ECS service already carries for its subnets while its cluster places it. +- **`AwsRedshiftServerlessWorkgroupEndpointAccess.subnet_ids` is containment-exempt.** A managed VPC endpoint's subnets are the CONSUMING VPC's; the workgroup is exposed inside them, never deployed into them. +- **`AwsRedshiftClusterEndpointAuthorization.vpc_ids` is containment-exempt.** An endpoint authorization admits another account's VPCs to the cluster; the cluster never lives inside a VPC it authorizes. The cluster's own `subnet_ids` stay placement. +- The containment-decision registry (`shared/cloudresourcekind/testdata/containment_decisions.txt`) moves exactly those three lines from `contained` to `exempt`; nothing else in the registry moved. + +## Why + +`container_kind` says a kind is a box other resources nest inside, and `containment_exempt` says a reference into such a box is access, not placement. The workgroup carried two placement claims into two rooms that cannot nest, which is the one shape the doctrine cannot draw truthfully. Its spec already says which room is home ("a workgroup computes; the data it serves lives on the namespace it attaches to"), so the subnets are the access path and the exemption records it. The other two fields were placement by omission on references that only ever meant "let these reach me". + +## How to check + +```bash +go test ./shared/cloudresourcekind/... -run TestContainmentDecisions # green; the golden carries the three exempt lines +grep -n containment_exempt catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto catalog/aws/awsredshiftcluster/v1alpha1/spec.proto +``` diff --git a/catalog/aws/awsredshiftcluster/v1alpha1/spec.pb.go b/catalog/aws/awsredshiftcluster/v1alpha1/spec.pb.go index 70bb55374f..66ec9c3d40 100644 --- a/catalog/aws/awsredshiftcluster/v1alpha1/spec.pb.go +++ b/catalog/aws/awsredshiftcluster/v1alpha1/spec.pb.go @@ -1265,6 +1265,9 @@ type AwsRedshiftClusterEndpointAuthorization struct { // Restrict the grant to specific VPCs in the grantee account. Empty // authorizes ALL of the account's VPCs. Reference AwsVpc vpc_id // outputs or pass literal VPC IDs. + // + // Containment-exempt: a grant admits another VPC's endpoints to the + // cluster; the cluster never lives inside the VPC it authorizes. VpcIds []*v1.StringValueOrRef `protobuf:"bytes,2,rep,name=vpc_ids,json=vpcIds,proto3" json:"vpc_ids,omitempty"` // Revoke the authorization on delete even if the grantee still has // live endpoints against the cluster (their endpoints are deleted @@ -1462,10 +1465,10 @@ const file_catalog_aws_awsredshiftcluster_v1alpha1_spec_proto_rawDesc = "" + " AwsRedshiftClusterEndpointAccess\x12@\n" + "\rendpoint_name\x18\x01 \x01(\tB\x1b\xbaH\x18\xc8\x01\x01r\x132\x11^[0-9a-z-]{1,30}$R\fendpointName\x12*\n" + "\x11subnet_group_name\x18\x02 \x01(\tR\x0fsubnetGroupName\x12\x92\x01\n" + - "\x16vpc_security_group_ids\x18\x03 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB)\x88\xd4a\xf7\a\x92\xd4a status.outputs.security_group_idR\x13vpcSecurityGroupIds\"\xea\x01\n" + + "\x16vpc_security_group_ids\x18\x03 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB)\x88\xd4a\xf7\a\x92\xd4a status.outputs.security_group_idR\x13vpcSecurityGroupIds\"\xee\x01\n" + "'AwsRedshiftClusterEndpointAuthorization\x12/\n" + - "\aaccount\x18\x01 \x01(\tB\x15\xbaH\x12\xc8\x01\x01r\r2\v^[0-9]{12}$R\aaccount\x12k\n" + - "\avpc_ids\x18\x02 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB\x1e\x88\xd4a\xf8\a\x92\xd4a\x15status.outputs.vpc_idR\x06vpcIds\x12!\n" + + "\aaccount\x18\x01 \x01(\tB\x15\xbaH\x12\xc8\x01\x01r\r2\v^[0-9]{12}$R\aaccount\x12o\n" + + "\avpc_ids\x18\x02 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB\"\x88\xd4a\xf8\a\x92\xd4a\x15status.outputs.vpc_id\x98\xd4a\x01R\x06vpcIds\x12!\n" + "\fforce_delete\x18\x03 \x01(\bR\vforceDeleteB\xee\x02\n" + "/com.dev.planton.aws.awsredshiftcluster.v1alpha1B\tSpecProtoP\x01Z_github.com/plantonhq/planton/catalog/aws/awsredshiftcluster/v1alpha1;awsredshiftclusterv1alpha1\xa2\x02\x04DPAA\xaa\x02+Dev.Planton.Aws.Awsredshiftcluster.V1alpha1\xca\x02+Dev\\Planton\\Aws\\Awsredshiftcluster\\V1alpha1\xe2\x027Dev\\Planton\\Aws\\Awsredshiftcluster\\V1alpha1\\GPBMetadata\xea\x02/Dev::Planton::Aws::Awsredshiftcluster::V1alpha1b\x06proto3" diff --git a/catalog/aws/awsredshiftcluster/v1alpha1/spec.proto b/catalog/aws/awsredshiftcluster/v1alpha1/spec.proto index 2e298eb4d8..57d070b3de 100644 --- a/catalog/aws/awsredshiftcluster/v1alpha1/spec.proto +++ b/catalog/aws/awsredshiftcluster/v1alpha1/spec.proto @@ -831,9 +831,13 @@ message AwsRedshiftClusterEndpointAuthorization { // Restrict the grant to specific VPCs in the grantee account. Empty // authorizes ALL of the account's VPCs. Reference AwsVpc vpc_id // outputs or pass literal VPC IDs. + // + // Containment-exempt: a grant admits another VPC's endpoints to the + // cluster; the cluster never lives inside the VPC it authorizes. repeated dev.planton.shared.foreignkey.v1.StringValueOrRef vpc_ids = 2 [ (dev.planton.shared.foreignkey.v1.default_kind) = AwsVpc, - (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.vpc_id" + (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.vpc_id", + (dev.planton.shared.foreignkey.v1.containment_exempt) = true ]; // Revoke the authorization on delete even if the grantee still has diff --git a/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.pb.go b/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.pb.go index 50e437453a..9f4684c468 100644 --- a/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.pb.go +++ b/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.pb.go @@ -78,6 +78,12 @@ type AwsRedshiftServerlessWorkgroupSpec struct { // base capacity. Empty lets AWS use the account's default VPC (only // meaningful in accounts that still have one). Reference AwsSubnet // subnet_id outputs or pass literal subnet IDs. + // + // Containment-exempt: on a diagram the workgroup lives inside the + // namespace it serves (the box AWS's own model draws around its + // workgroups); the subnets are where its network interfaces land, and + // the workgroup reaches into that VPC by lines -- the same verdict an + // ECS service carries for its subnets while its cluster places it. SubnetIds []*v1.StringValueOrRef `protobuf:"bytes,6,rep,name=subnet_ids,json=subnetIds,proto3" json:"subnet_ids,omitempty"` // Security groups attached to the workgroup's endpoint. Empty uses // the VPC's default security group (the AWS default). Reference @@ -457,6 +463,9 @@ type AwsRedshiftServerlessWorkgroupEndpointAccess struct { // subnet_ids (which must then be set -- CEL-enforced). Reference // AwsSubnet subnet_id outputs or pass literal subnet IDs. Changing // the list replaces the endpoint. + // + // Containment-exempt: these are the consuming VPC's subnets, an access + // path INTO the workgroup; the workgroup is not deployed into them. SubnetIds []*v1.StringValueOrRef `protobuf:"bytes,2,rep,name=subnet_ids,json=subnetIds,proto3" json:"subnet_ids,omitempty"` // Security groups attached to the endpoint's network interfaces. // Empty uses the VPC's default security group. Reference @@ -606,15 +615,15 @@ var File_catalog_aws_awsredshiftserverlessworkgroup_v1alpha1_spec_proto protoref const file_catalog_aws_awsredshiftserverlessworkgroup_v1alpha1_spec_proto_rawDesc = "" + "\n" + - ">catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto\x127dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a&shared/foreignkey/v1/foreign_key.proto\"\xdd\x1a\n" + + ">catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto\x127dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a&shared/foreignkey/v1/foreign_key.proto\"\xe1\x1a\n" + "\"AwsRedshiftServerlessWorkgroupSpec\x12\x1f\n" + "\x06region\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x06region\x12\x87\x01\n" + "\x0enamespace_name\x18\x02 \x01(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB,\xbaH\x03\xc8\x01\x01\x88\xd4a\x95\b\x92\xd4a\x1dstatus.outputs.namespace_nameR\rnamespaceName\x12#\n" + "\rbase_capacity\x18\x03 \x01(\x05R\fbaseCapacity\x12!\n" + "\fmax_capacity\x18\x04 \x01(\x05R\vmaxCapacity\x12\xa7\x01\n" + - "\x18price_performance_target\x18\x05 \x01(\v2m.dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupPricePerformanceTargetR\x16pricePerformanceTarget\x12t\n" + + "\x18price_performance_target\x18\x05 \x01(\v2m.dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupPricePerformanceTargetR\x16pricePerformanceTarget\x12x\n" + "\n" + - "subnet_ids\x18\x06 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB!\x88\xd4a\xbc\b\x92\xd4a\x18status.outputs.subnet_idR\tsubnetIds\x12\x8b\x01\n" + + "subnet_ids\x18\x06 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB%\x88\xd4a\xbc\b\x92\xd4a\x18status.outputs.subnet_id\x98\xd4a\x01R\tsubnetIds\x12\x8b\x01\n" + "\x12security_group_ids\x18\a \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB)\x88\xd4a\xf7\a\x92\xd4a status.outputs.security_group_idR\x10securityGroupIds\x120\n" + "\x14enhanced_vpc_routing\x18\b \x01(\bR\x12enhancedVpcRouting\x12/\n" + "\x13publicly_accessible\x18\t \x01(\bR\x12publiclyAccessible\x12\x12\n" + @@ -645,11 +654,11 @@ const file_catalog_aws_awsredshiftserverlessworkgroup_v1alpha1_spec_proto_rawDes "*AwsRedshiftServerlessWorkgroupCustomDomain\x12,\n" + "\vdomain_name\x18\x01 \x01(\tB\v\xbaH\b\xc8\x01\x01r\x03\x18\xfd\x01R\n" + "domainName\x12\x83\x01\n" + - "\x0fcertificate_arn\x18\x02 \x01(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB&\xbaH\x03\xc8\x01\x01\x88\xd4a\xe9\a\x92\xd4a\x17status.outputs.cert_arnR\x0ecertificateArn\"\xec\x02\n" + + "\x0fcertificate_arn\x18\x02 \x01(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB&\xbaH\x03\xc8\x01\x01\x88\xd4a\xe9\a\x92\xd4a\x17status.outputs.cert_arnR\x0ecertificateArn\"\xf0\x02\n" + ",AwsRedshiftServerlessWorkgroupEndpointAccess\x121\n" + - "\rendpoint_name\x18\x01 \x01(\tB\f\xbaH\t\xc8\x01\x01r\x04\x10\x01\x18\x1eR\fendpointName\x12t\n" + + "\rendpoint_name\x18\x01 \x01(\tB\f\xbaH\t\xc8\x01\x01r\x04\x10\x01\x18\x1eR\fendpointName\x12x\n" + "\n" + - "subnet_ids\x18\x02 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB!\x88\xd4a\xbc\b\x92\xd4a\x18status.outputs.subnet_idR\tsubnetIds\x12\x92\x01\n" + + "subnet_ids\x18\x02 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB%\x88\xd4a\xbc\b\x92\xd4a\x18status.outputs.subnet_id\x98\xd4a\x01R\tsubnetIds\x12\x92\x01\n" + "\x16vpc_security_group_ids\x18\x03 \x03(\v22.dev.planton.shared.foreignkey.v1.StringValueOrRefB)\x88\xd4a\xf7\a\x92\xd4a status.outputs.security_group_idR\x13vpcSecurityGroupIds\"\xa9\x02\n" + "(AwsRedshiftServerlessWorkgroupUsageLimit\x12U\n" + "\n" + diff --git a/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto b/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto index 730b82b6f7..61a71225b3 100644 --- a/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto +++ b/catalog/aws/awsredshiftserverlessworkgroup/v1alpha1/spec.proto @@ -68,9 +68,16 @@ message AwsRedshiftServerlessWorkgroupSpec { // base capacity. Empty lets AWS use the account's default VPC (only // meaningful in accounts that still have one). Reference AwsSubnet // subnet_id outputs or pass literal subnet IDs. + // + // Containment-exempt: on a diagram the workgroup lives inside the + // namespace it serves (the box AWS's own model draws around its + // workgroups); the subnets are where its network interfaces land, and + // the workgroup reaches into that VPC by lines -- the same verdict an + // ECS service carries for its subnets while its cluster places it. repeated dev.planton.shared.foreignkey.v1.StringValueOrRef subnet_ids = 6 [ (dev.planton.shared.foreignkey.v1.default_kind) = AwsSubnet, - (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.subnet_id" + (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.subnet_id", + (dev.planton.shared.foreignkey.v1.containment_exempt) = true ]; // Security groups attached to the workgroup's endpoint. Empty uses @@ -308,9 +315,13 @@ message AwsRedshiftServerlessWorkgroupEndpointAccess { // subnet_ids (which must then be set -- CEL-enforced). Reference // AwsSubnet subnet_id outputs or pass literal subnet IDs. Changing // the list replaces the endpoint. + // + // Containment-exempt: these are the consuming VPC's subnets, an access + // path INTO the workgroup; the workgroup is not deployed into them. repeated dev.planton.shared.foreignkey.v1.StringValueOrRef subnet_ids = 2 [ (dev.planton.shared.foreignkey.v1.default_kind) = AwsSubnet, - (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.subnet_id" + (dev.planton.shared.foreignkey.v1.default_kind_field_path) = "status.outputs.subnet_id", + (dev.planton.shared.foreignkey.v1.containment_exempt) = true ]; // Security groups attached to the endpoint's network interfaces. diff --git a/shared/cloudresourcekind/testdata/containment_decisions.txt b/shared/cloudresourcekind/testdata/containment_decisions.txt index 8a7f8a4f62..12cf60b756 100644 --- a/shared/cloudresourcekind/testdata/containment_decisions.txt +++ b/shared/cloudresourcekind/testdata/containment_decisions.txt @@ -86,11 +86,8 @@ contained dev.planton.aws.awsrdsinstance.v1alpha1.AwsRdsInstanceSpec.subnet_ids contained dev.planton.aws.awsrdsproxy.v1alpha1.AwsRdsProxyEndpoint.vpc_subnet_ids -> AwsSubnet contained dev.planton.aws.awsrdsproxy.v1alpha1.AwsRdsProxySpec.vpc_subnet_ids -> AwsSubnet contained dev.planton.aws.awsrediselasticache.v1alpha1.AwsRedisElasticacheSpec.subnet_ids -> AwsSubnet -contained dev.planton.aws.awsredshiftcluster.v1alpha1.AwsRedshiftClusterEndpointAuthorization.vpc_ids -> AwsVpc contained dev.planton.aws.awsredshiftcluster.v1alpha1.AwsRedshiftClusterSpec.subnet_ids -> AwsSubnet -contained dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupEndpointAccess.subnet_ids -> AwsSubnet contained dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupSpec.namespace_name -> AwsRedshiftServerlessNamespace -contained dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupSpec.subnet_ids -> AwsSubnet contained dev.planton.aws.awsrestapigateway.v1alpha1.AwsRestApiGatewayAuthorizer.provider_arns -> AwsCognitoUserPool contained dev.planton.aws.awsroute53dnsrecord.v1alpha1.AwsRoute53DnsRecordSpec.zone_id -> AwsRoute53Zone contained dev.planton.aws.awsroute53resolverendpoint.v1alpha1.AwsRoute53ResolverEndpointIpAddress.subnet_id -> AwsSubnet @@ -678,6 +675,9 @@ exempt dev.planton.aws.awslblistenerrule.v1alpha1.AwsLbListenerRuleActionAuth exempt dev.planton.aws.awslblistenerrule.v1alpha1.AwsLbListenerRuleActionAuthenticateCognito.user_pool_domain -> AwsCognitoUserPool exempt dev.planton.aws.awsnlb.v1alpha1.AwsNlbDns.route53_zone_id -> AwsRoute53Zone exempt dev.planton.aws.awsopensearchdomain.v1alpha1.AwsOpenSearchDomainCognitoOptions.user_pool_id -> AwsCognitoUserPool +exempt dev.planton.aws.awsredshiftcluster.v1alpha1.AwsRedshiftClusterEndpointAuthorization.vpc_ids -> AwsVpc +exempt dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupEndpointAccess.subnet_ids -> AwsSubnet +exempt dev.planton.aws.awsredshiftserverlessworkgroup.v1alpha1.AwsRedshiftServerlessWorkgroupSpec.subnet_ids -> AwsSubnet exempt dev.planton.aws.awsroute53zone.v1alpha1.AwsRoute53ZoneVpcAssociation.vpc_id -> AwsVpc exempt dev.planton.aws.awssagemakerdomain.v1alpha1.AwsSagemakerDomainEfsFileSystemConfig.file_system_id -> AwsElasticFileSystem exempt dev.planton.aws.awssesconfigurationset.v1alpha1.AwsSesConfigurationSetEventDestination.event_bus -> AwsEventBridgeBus