From e2ba0b8871104c0e93a0797691a0faf4762d9fb5 Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Fri, 12 Jun 2026 13:37:47 -0500 Subject: [PATCH 1/7] OSAC-463: Add delete referential integrity tests for VirtualNetwork --- .../private_virtual_networks_server_test.go | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/internal/servers/private_virtual_networks_server_test.go b/internal/servers/private_virtual_networks_server_test.go index d54b35d2a..e0608eb5b 100644 --- a/internal/servers/private_virtual_networks_server_test.go +++ b/internal/servers/private_virtual_networks_server_test.go @@ -1363,4 +1363,237 @@ var _ = Describe("Private virtual networks server", func() { Expect(err.Error()).To(ContainSubstring("immutable")) }) }) + + Describe("Deletion referential integrity", func() { + var ( + vnServer *PrivateVirtualNetworksServer + subnetDao *dao.GenericDAO[*privatev1.Subnet] + sgDao *dao.GenericDAO[*privatev1.SecurityGroup] + ) + + BeforeEach(func() { + var err error + vnServer, err = NewPrivateVirtualNetworksServer(). + SetLogger(logger). + SetAttributionLogic(attribution). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + + subnetDao, err = dao.NewGenericDAO[*privatev1.Subnet](). + SetLogger(logger). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + + sgDao, err = dao.NewGenericDAO[*privatev1.SecurityGroup](). + SetLogger(logger). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + }) + + createVirtualNetwork := func() *privatev1.VirtualNetwork { + nc := createNetworkClass(ctx, privatev1.NetworkClassState_NETWORK_CLASS_STATE_READY) + createResp, err := vnServer.Create(ctx, privatev1.VirtualNetworksCreateRequest_builder{ + Object: privatev1.VirtualNetwork_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + }.Build(), + Spec: privatev1.VirtualNetworkSpec_builder{ + Ipv4Cidr: proto.String("10.0.0.0/16"), + NetworkClass: nc.GetId(), + Region: "us-west-1", + }.Build(), + }.Build(), + }.Build()) + Expect(err).ToNot(HaveOccurred()) + return createResp.GetObject() + } + + setVirtualNetworkReady := func(vn *privatev1.VirtualNetwork) { + vnDao, err := dao.NewGenericDAO[*privatev1.VirtualNetwork](). + SetLogger(logger). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + vn.SetStatus(privatev1.VirtualNetworkStatus_builder{ + State: privatev1.VirtualNetworkState_VIRTUAL_NETWORK_STATE_READY, + }.Build()) + _, err = vnDao.Update().SetObject(vn).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + } + + It("allows deletion when no child resources exist", func() { + vn := createVirtualNetwork() + + _, err := vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).ToNot(HaveOccurred()) + }) + + It("blocks deletion when a Subnet references the VirtualNetwork", func() { + vn := createVirtualNetwork() + setVirtualNetworkReady(vn) + + subnet := privatev1.Subnet_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SubnetSpec_builder{ + VirtualNetwork: vn.GetId(), + Ipv4Cidr: proto.String("10.0.1.0/24"), + }.Build(), + }.Build() + _, err := subnetDao.Create().SetObject(subnet).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).To(HaveOccurred()) + status, ok := grpcstatus.FromError(err) + Expect(ok).To(BeTrue()) + Expect(status.Code()).To(Equal(grpccodes.FailedPrecondition)) + Expect(err.Error()).To(ContainSubstring("Subnet")) + Expect(err.Error()).To(ContainSubstring(vn.GetId())) + }) + + It("blocks deletion when a SecurityGroup references the VirtualNetwork", func() { + vn := createVirtualNetwork() + + sg := privatev1.SecurityGroup_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SecurityGroupSpec_builder{ + VirtualNetwork: vn.GetId(), + }.Build(), + }.Build() + _, err := sgDao.Create().SetObject(sg).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).To(HaveOccurred()) + status, ok := grpcstatus.FromError(err) + Expect(ok).To(BeTrue()) + Expect(status.Code()).To(Equal(grpccodes.FailedPrecondition)) + Expect(err.Error()).To(ContainSubstring("SecurityGroup")) + Expect(err.Error()).To(ContainSubstring(vn.GetId())) + }) + + It("blocks deletion when both Subnets and SecurityGroups reference the VirtualNetwork", func() { + vn := createVirtualNetwork() + setVirtualNetworkReady(vn) + + subnet := privatev1.Subnet_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SubnetSpec_builder{ + VirtualNetwork: vn.GetId(), + Ipv4Cidr: proto.String("10.0.1.0/24"), + }.Build(), + }.Build() + _, err := subnetDao.Create().SetObject(subnet).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + sg := privatev1.SecurityGroup_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SecurityGroupSpec_builder{ + VirtualNetwork: vn.GetId(), + }.Build(), + }.Build() + _, err = sgDao.Create().SetObject(sg).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).To(HaveOccurred()) + status, ok := grpcstatus.FromError(err) + Expect(ok).To(BeTrue()) + Expect(status.Code()).To(Equal(grpccodes.FailedPrecondition)) + Expect(err.Error()).To(ContainSubstring("Subnet")) + }) + + It("allows deletion after all child resources are removed", func() { + vn := createVirtualNetwork() + + sg := privatev1.SecurityGroup_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SecurityGroupSpec_builder{ + VirtualNetwork: vn.GetId(), + }.Build(), + }.Build() + sgResp, err := sgDao.Create().SetObject(sg).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).To(HaveOccurred()) + + _, err = sgDao.Delete().SetId(sgResp.GetObject().GetId()).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + + _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).ToNot(HaveOccurred()) + }) + + It("reports the correct count of referencing Subnets", func() { + vn := createVirtualNetwork() + setVirtualNetworkReady(vn) + + for i := range 3 { + subnet := privatev1.Subnet_builder{ + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + Annotations: map[string]string{ + "osac.io/owner-reference": vn.GetId(), + }, + }.Build(), + Spec: privatev1.SubnetSpec_builder{ + VirtualNetwork: vn.GetId(), + Ipv4Cidr: proto.String(fmt.Sprintf("10.0.%d.0/24", i)), + }.Build(), + }.Build() + _, err := subnetDao.Create().SetObject(subnet).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + } + + _, err := vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ + Id: vn.GetId(), + }.Build()) + Expect(err).To(HaveOccurred()) + status, ok := grpcstatus.FromError(err) + Expect(ok).To(BeTrue()) + Expect(status.Code()).To(Equal(grpccodes.FailedPrecondition)) + Expect(err.Error()).To(ContainSubstring("3 Subnet")) + }) + }) }) From 84e2d43adc99ae590ad71d7fdadf62ab38f9bcf8 Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Fri, 12 Jun 2026 13:38:40 -0500 Subject: [PATCH 2/7] OSAC-463: Block VirtualNetwork delete when Subnets or SecurityGroups reference it --- .../private_virtual_networks_server.go | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/internal/servers/private_virtual_networks_server.go b/internal/servers/private_virtual_networks_server.go index 53aca07cc..16005f84d 100644 --- a/internal/servers/private_virtual_networks_server.go +++ b/internal/servers/private_virtual_networks_server.go @@ -43,9 +43,11 @@ var _ privatev1.VirtualNetworksServer = (*PrivateVirtualNetworksServer)(nil) type PrivateVirtualNetworksServer struct { privatev1.UnimplementedVirtualNetworksServer - logger *slog.Logger - generic *GenericServer[*privatev1.VirtualNetwork] - networkClassDao *dao.GenericDAO[*privatev1.NetworkClass] + logger *slog.Logger + generic *GenericServer[*privatev1.VirtualNetwork] + networkClassDao *dao.GenericDAO[*privatev1.NetworkClass] + subnetDao *dao.GenericDAO[*privatev1.Subnet] + securityGroupDao *dao.GenericDAO[*privatev1.SecurityGroup] } func NewPrivateVirtualNetworksServer() *PrivateVirtualNetworksServerBuilder { @@ -100,6 +102,26 @@ func (b *PrivateVirtualNetworksServerBuilder) Build() (result *PrivateVirtualNet return } + // Create the Subnet DAO for child reference checks on delete: + subnetDao, err := dao.NewGenericDAO[*privatev1.Subnet](). + SetLogger(b.logger). + SetTenancyLogic(b.tenancyLogic). + SetMetricsRegisterer(b.metricsRegisterer). + Build() + if err != nil { + return + } + + // Create the SecurityGroup DAO for child reference checks on delete: + securityGroupDao, err := dao.NewGenericDAO[*privatev1.SecurityGroup](). + SetLogger(b.logger). + SetTenancyLogic(b.tenancyLogic). + SetMetricsRegisterer(b.metricsRegisterer). + Build() + if err != nil { + return + } + // Create the generic server: generic, err := NewGenericServer[*privatev1.VirtualNetwork](). SetLogger(b.logger). @@ -115,9 +137,11 @@ func (b *PrivateVirtualNetworksServerBuilder) Build() (result *PrivateVirtualNet // Create and populate the object: result = &PrivateVirtualNetworksServer{ - logger: b.logger, - generic: generic, - networkClassDao: networkClassDao, + logger: b.logger, + generic: generic, + networkClassDao: networkClassDao, + subnetDao: subnetDao, + securityGroupDao: securityGroupDao, } return } @@ -188,10 +212,57 @@ func (s *PrivateVirtualNetworksServer) Update(ctx context.Context, func (s *PrivateVirtualNetworksServer) Delete(ctx context.Context, request *privatev1.VirtualNetworksDeleteRequest) (response *privatev1.VirtualNetworksDeleteResponse, err error) { + if err = s.checkNoChildReferences(ctx, request.GetId()); err != nil { + return + } + err = s.generic.Delete(ctx, request, &response) return } +// checkNoChildReferences verifies that no Subnets or SecurityGroups reference the given VirtualNetwork. +func (s *PrivateVirtualNetworksServer) checkNoChildReferences(ctx context.Context, virtualNetworkID string) error { + if virtualNetworkID == "" { + return nil + } + + filter := fmt.Sprintf("this.spec.virtual_network == %q", virtualNetworkID) + + subnetResponse, err := s.subnetDao.List(). + SetFilter(filter). + SetLimit(1). + Do(ctx) + if err != nil { + s.logger.ErrorContext(ctx, "Failed to check subnet references", + slog.String("virtual_network_id", virtualNetworkID), + slog.Any("error", err)) + return grpcstatus.Errorf(grpccodes.Internal, "failed to check child references") + } + if subnetResponse.GetTotal() > 0 { + return grpcstatus.Errorf(grpccodes.FailedPrecondition, + "cannot delete VirtualNetwork '%s': %d Subnet(s) still reference it", + virtualNetworkID, subnetResponse.GetTotal()) + } + + sgResponse, err := s.securityGroupDao.List(). + SetFilter(filter). + SetLimit(1). + Do(ctx) + if err != nil { + s.logger.ErrorContext(ctx, "Failed to check security group references", + slog.String("virtual_network_id", virtualNetworkID), + slog.Any("error", err)) + return grpcstatus.Errorf(grpccodes.Internal, "failed to check child references") + } + if sgResponse.GetTotal() > 0 { + return grpcstatus.Errorf(grpccodes.FailedPrecondition, + "cannot delete VirtualNetwork '%s': %d SecurityGroup(s) still reference it", + virtualNetworkID, sgResponse.GetTotal()) + } + + return nil +} + func (s *PrivateVirtualNetworksServer) Signal(ctx context.Context, request *privatev1.VirtualNetworksSignalRequest) (response *privatev1.VirtualNetworksSignalResponse, err error) { err = s.generic.Signal(ctx, request, &response) From b100e1f9159a9e7740a53d856045b3f1dcdc24d6 Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Fri, 12 Jun 2026 14:15:00 -0500 Subject: [PATCH 3/7] OSAC-463: Address CodeRabbit review feedback Add docstrings for Delete and checkNoChildReferences, and remove unnecessary owner-reference annotations from deletion guard tests. --- .../servers/private_virtual_networks_server.go | 5 ++++- .../private_virtual_networks_server_test.go | 18 ------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/internal/servers/private_virtual_networks_server.go b/internal/servers/private_virtual_networks_server.go index 16005f84d..96cd05586 100644 --- a/internal/servers/private_virtual_networks_server.go +++ b/internal/servers/private_virtual_networks_server.go @@ -210,6 +210,8 @@ func (s *PrivateVirtualNetworksServer) Update(ctx context.Context, return } +// Delete removes a VirtualNetwork after verifying no child Subnets or SecurityGroups reference it. +// Returns FailedPrecondition when references exist. func (s *PrivateVirtualNetworksServer) Delete(ctx context.Context, request *privatev1.VirtualNetworksDeleteRequest) (response *privatev1.VirtualNetworksDeleteResponse, err error) { if err = s.checkNoChildReferences(ctx, request.GetId()); err != nil { @@ -220,7 +222,8 @@ func (s *PrivateVirtualNetworksServer) Delete(ctx context.Context, return } -// checkNoChildReferences verifies that no Subnets or SecurityGroups reference the given VirtualNetwork. +// checkNoChildReferences returns nil when no Subnets or SecurityGroups reference virtualNetworkID. +// Returns FailedPrecondition when child resources exist, or Internal when the reference query fails. func (s *PrivateVirtualNetworksServer) checkNoChildReferences(ctx context.Context, virtualNetworkID string) error { if virtualNetworkID == "" { return nil diff --git a/internal/servers/private_virtual_networks_server_test.go b/internal/servers/private_virtual_networks_server_test.go index e0608eb5b..223c2c789 100644 --- a/internal/servers/private_virtual_networks_server_test.go +++ b/internal/servers/private_virtual_networks_server_test.go @@ -1440,9 +1440,6 @@ var _ = Describe("Private virtual networks server", func() { subnet := privatev1.Subnet_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SubnetSpec_builder{ VirtualNetwork: vn.GetId(), @@ -1469,9 +1466,6 @@ var _ = Describe("Private virtual networks server", func() { sg := privatev1.SecurityGroup_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SecurityGroupSpec_builder{ VirtualNetwork: vn.GetId(), @@ -1498,9 +1492,6 @@ var _ = Describe("Private virtual networks server", func() { subnet := privatev1.Subnet_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SubnetSpec_builder{ VirtualNetwork: vn.GetId(), @@ -1513,9 +1504,6 @@ var _ = Describe("Private virtual networks server", func() { sg := privatev1.SecurityGroup_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SecurityGroupSpec_builder{ VirtualNetwork: vn.GetId(), @@ -1540,9 +1528,6 @@ var _ = Describe("Private virtual networks server", func() { sg := privatev1.SecurityGroup_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SecurityGroupSpec_builder{ VirtualNetwork: vn.GetId(), @@ -1573,9 +1558,6 @@ var _ = Describe("Private virtual networks server", func() { subnet := privatev1.Subnet_builder{ Metadata: privatev1.Metadata_builder{ Tenant: auth.SharedTenant, - Annotations: map[string]string{ - "osac.io/owner-reference": vn.GetId(), - }, }.Build(), Spec: privatev1.SubnetSpec_builder{ VirtualNetwork: vn.GetId(), From 97546829fa3d6b6df54828928391e2e4ab22c88f Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Mon, 15 Jun 2026 14:09:12 -0500 Subject: [PATCH 4/7] OSAC-463: Enforce VN child refs with DB triggers per OSAC-879 pattern Replace the application-layer delete guard with bidirectional PostgreSQL triggers (migration 54): block virtual network soft-delete when active subnets or security groups reference it, and validate child inserts with FOR SHARE locking to close TOCTOU races. --- ..._virtual_network_child_ref_triggers.up.sql | 151 +++++++++ ...virtual_network_child_ref_triggers_test.go | 315 ++++++++++++++++++ .../private_virtual_networks_server.go | 86 +---- .../private_virtual_networks_server_test.go | 7 +- 4 files changed, 473 insertions(+), 86 deletions(-) create mode 100644 internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql create mode 100644 internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go diff --git a/internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql b/internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql new file mode 100644 index 000000000..5457e56ec --- /dev/null +++ b/internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql @@ -0,0 +1,151 @@ +-- +-- Copyright (c) 2026 Red Hat Inc. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +-- an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations under the License. +-- + +-- This migration adds triggers that enforce bidirectional referential integrity between virtual networks and their +-- child subnets and security groups: +-- +-- 1. A BEFORE UPDATE trigger on virtual_networks that prevents soft-deleting a virtual network while active subnets +-- or security groups still reference it. +-- +-- 2. BEFORE INSERT triggers on subnets and security_groups that prevent creating a child that references a virtual +-- network that does not exist or has been soft-deleted. Each trigger uses FOR SHARE to acquire a shared lock on +-- the virtual network row, which conflicts with the exclusive lock held by a concurrent virtual network +-- soft-delete. This bidirectional locking eliminates the TOCTOU race condition between virtual network deletion +-- and child creation. The triggers only fire on INSERT because virtual_network references are immutable +-- (enforced by the application). + +-- Indexes to speed up child reference lookups used by the delete trigger: +create index subnets_by_virtual_network on subnets ((data->'spec'->>'virtual_network')) + where deletion_timestamp = 'epoch'; + +create index security_groups_by_virtual_network on security_groups ((data->'spec'->>'virtual_network')) + where deletion_timestamp = 'epoch'; + +-- Trigger function that checks whether any active subnet or security group references the virtual network being deleted: +create function check_virtual_network_not_in_use() returns trigger as $$ +declare + subnet_count bigint; + sg_count bigint; +begin + select count(*) into subnet_count + from subnets + where deletion_timestamp = 'epoch' + and data->'spec'->>'virtual_network' = old.id; + + if subnet_count > 0 then + raise exception using + errcode = 'Z0003', + message = format( + 'cannot delete VirtualNetwork ''%s'': %s Subnet(s) still reference it', + old.id, subnet_count + ); + end if; + + select count(*) into sg_count + from security_groups + where deletion_timestamp = 'epoch' + and data->'spec'->>'virtual_network' = old.id; + + if sg_count > 0 then + raise exception using + errcode = 'Z0003', + message = format( + 'cannot delete VirtualNetwork ''%s'': %s SecurityGroup(s) still reference it', + old.id, sg_count + ); + end if; + + return new; +end; +$$ language plpgsql; + +-- Attach the trigger so that it only fires when the row transitions from active to soft-deleted: +create trigger check_virtual_network_not_in_use + before update on virtual_networks + for each row + when (old.deletion_timestamp = 'epoch' and new.deletion_timestamp != 'epoch') + execute function check_virtual_network_not_in_use(); + +-- Trigger function that validates the virtual_network reference in a subnet: +create function check_subnet_virtual_network_ref() returns trigger as $$ +declare + vn_id text; + found_id text; +begin + vn_id := new.data->'spec'->>'virtual_network'; + if coalesce(vn_id, '') = '' then + return new; + end if; + + select id into found_id + from virtual_networks + where id = vn_id + and deletion_timestamp = 'epoch' + for share; + + if found_id is null then + raise exception using + errcode = 'Z0002', + message = format( + 'VirtualNetwork ''%s'' does not exist or has been deleted', + vn_id + ); + end if; + + return new; +end; +$$ language plpgsql; + +-- Attach the trigger so that it fires on insert of active subnets: +create trigger check_subnet_virtual_network_ref + before insert on subnets + for each row + when (new.deletion_timestamp = 'epoch') + execute function check_subnet_virtual_network_ref(); + +-- Trigger function that validates the virtual_network reference in a security group: +create function check_security_group_virtual_network_ref() returns trigger as $$ +declare + vn_id text; + found_id text; +begin + vn_id := new.data->'spec'->>'virtual_network'; + if coalesce(vn_id, '') = '' then + return new; + end if; + + select id into found_id + from virtual_networks + where id = vn_id + and deletion_timestamp = 'epoch' + for share; + + if found_id is null then + raise exception using + errcode = 'Z0002', + message = format( + 'VirtualNetwork ''%s'' does not exist or has been deleted', + vn_id + ); + end if; + + return new; +end; +$$ language plpgsql; + +-- Attach the trigger so that it fires on insert of active security groups: +create trigger check_security_group_virtual_network_ref + before insert on security_groups + for each row + when (new.deletion_timestamp = 'epoch') + execute function check_security_group_virtual_network_ref(); diff --git a/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go b/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go new file mode 100644 index 000000000..a32ff38f4 --- /dev/null +++ b/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go @@ -0,0 +1,315 @@ +/* +Copyright (c) 2026 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package migrations + +import ( + "context" + "errors" + "fmt" + + "github.com/jackc/pgx/v5/pgconn" + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/gomega" +) + +var _ = DescribeMigration("Add virtual network child ref triggers", func() { + insertTenant := func(ctx context.Context) { + _, err := conn.Exec(ctx, + `insert into organizations (id, name, tenant, creator, data) + values ('test-tenant', 'test-tenant', 'system', 'system', '{}') + on conflict do nothing`) + Expect(err).ToNot(HaveOccurred()) + } + + It("Creates the 'check_virtual_network_not_in_use' function", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + + var count int + err = conn.QueryRow(ctx, ` + select + count(*) + from + information_schema.routines + where + routine_name = 'check_virtual_network_not_in_use' and + routine_type = 'FUNCTION' + `).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("Adds a delete trigger to the virtual_networks table", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + + var count int + err = conn.QueryRow(ctx, ` + select + count(*) + from + information_schema.triggers + where + trigger_name = 'check_virtual_network_not_in_use' and + event_object_table = 'virtual_networks' and + action_timing = 'BEFORE' and + event_manipulation = 'UPDATE' + `).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("Creates indexes for child virtual_network lookups", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + + for _, indexName := range []string{"subnets_by_virtual_network", "security_groups_by_virtual_network"} { + var count int + err = conn.QueryRow(ctx, ` + select + count(*) + from + pg_indexes + where + indexname = $1 + `, indexName).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + } + }) + + It("Prevents soft-deleting a virtual network referenced by a subnet", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-1', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `insert into subnets (id, tenant, data) + values ('subnet-1', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"vn-1"}}`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-1'`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0003")) + Expect(pgErr.Message).To(ContainSubstring("vn-1")) + Expect(pgErr.Message).To(ContainSubstring("Subnet")) + }) + + It("Prevents soft-deleting a virtual network referenced by a security group", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-2', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `insert into security_groups (id, tenant, data) + values ('sg-1', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"vn-2"}}`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-2'`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0003")) + Expect(pgErr.Message).To(ContainSubstring("vn-2")) + Expect(pgErr.Message).To(ContainSubstring("SecurityGroup")) + }) + + It("Reports the count of referencing subnets", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-3', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + + for i := range 3 { + _, err = conn.Exec(ctx, + `insert into subnets (id, tenant, data) + values ($1, 'test-tenant', $2::jsonb)`, + fmt.Sprintf("subnet-%d", i), + fmt.Sprintf(`{"spec":{"virtual_network":"vn-3","ipv4_cidr":"10.0.%d.0/24"}}`, i)) + Expect(err).ToNot(HaveOccurred()) + } + + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-3'`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0003")) + Expect(pgErr.Message).To(ContainSubstring("3 Subnet")) + }) + + It("Allows soft-deleting a virtual network with no child references", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-4', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-4'`) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Allows soft-deleting a virtual network when child subnets are already deleted", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-5', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `insert into subnets (id, tenant, data) + values ('subnet-del', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"vn-5"}}`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `update subnets set deletion_timestamp = now() where id = 'subnet-del'`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-5'`) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Creates the subnet virtual_network insert trigger", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + + var count int + err = conn.QueryRow(ctx, ` + select + count(*) + from + information_schema.triggers + where + trigger_name = 'check_subnet_virtual_network_ref' and + event_object_table = 'subnets' and + action_timing = 'BEFORE' and + event_manipulation = 'INSERT' + `).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("Prevents creating a subnet referencing a non-existent virtual network", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into subnets (id, tenant, data) + values ('subnet-ref', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"no-such-vn"}}`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0002")) + Expect(pgErr.Message).To(ContainSubstring("no-such-vn")) + }) + + It("Prevents creating a subnet referencing a soft-deleted virtual network", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-deleted', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-deleted'`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `insert into subnets (id, tenant, data) + values ('subnet-ref-2', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"vn-deleted"}}`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0002")) + Expect(pgErr.Message).To(ContainSubstring("vn-deleted")) + }) + + It("Creates the security group virtual_network insert trigger", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + + var count int + err = conn.QueryRow(ctx, ` + select + count(*) + from + information_schema.triggers + where + trigger_name = 'check_security_group_virtual_network_ref' and + event_object_table = 'security_groups' and + action_timing = 'BEFORE' and + event_manipulation = 'INSERT' + `).Scan(&count) + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) + }) + + It("Prevents creating a security group referencing a soft-deleted virtual network", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into virtual_networks (id, tenant, data) + values ('vn-sg-deleted', 'test-tenant', '{}')`) + Expect(err).ToNot(HaveOccurred()) + _, err = conn.Exec(ctx, + `update virtual_networks set deletion_timestamp = now() where id = 'vn-sg-deleted'`) + Expect(err).ToNot(HaveOccurred()) + + _, err = conn.Exec(ctx, + `insert into security_groups (id, tenant, data) + values ('sg-ref', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"vn-sg-deleted"}}`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0002")) + Expect(pgErr.Message).To(ContainSubstring("vn-sg-deleted")) + }) +}) diff --git a/internal/servers/private_virtual_networks_server.go b/internal/servers/private_virtual_networks_server.go index 1154e8c10..9e4f593a5 100644 --- a/internal/servers/private_virtual_networks_server.go +++ b/internal/servers/private_virtual_networks_server.go @@ -43,11 +43,9 @@ var _ privatev1.VirtualNetworksServer = (*PrivateVirtualNetworksServer)(nil) type PrivateVirtualNetworksServer struct { privatev1.UnimplementedVirtualNetworksServer - logger *slog.Logger - generic *GenericServer[*privatev1.VirtualNetwork] - networkClassDao *dao.GenericDAO[*privatev1.NetworkClass] - subnetDao *dao.GenericDAO[*privatev1.Subnet] - securityGroupDao *dao.GenericDAO[*privatev1.SecurityGroup] + logger *slog.Logger + generic *GenericServer[*privatev1.VirtualNetwork] + networkClassDao *dao.GenericDAO[*privatev1.NetworkClass] } func NewPrivateVirtualNetworksServer() *PrivateVirtualNetworksServerBuilder { @@ -102,26 +100,6 @@ func (b *PrivateVirtualNetworksServerBuilder) Build() (result *PrivateVirtualNet return } - // Create the Subnet DAO for child reference checks on delete: - subnetDao, err := dao.NewGenericDAO[*privatev1.Subnet](). - SetLogger(b.logger). - SetTenancyLogic(b.tenancyLogic). - SetMetricsRegisterer(b.metricsRegisterer). - Build() - if err != nil { - return - } - - // Create the SecurityGroup DAO for child reference checks on delete: - securityGroupDao, err := dao.NewGenericDAO[*privatev1.SecurityGroup](). - SetLogger(b.logger). - SetTenancyLogic(b.tenancyLogic). - SetMetricsRegisterer(b.metricsRegisterer). - Build() - if err != nil { - return - } - // Create the generic server: generic, err := NewGenericServer[*privatev1.VirtualNetwork](). SetLogger(b.logger). @@ -137,11 +115,9 @@ func (b *PrivateVirtualNetworksServerBuilder) Build() (result *PrivateVirtualNet // Create and populate the object: result = &PrivateVirtualNetworksServer{ - logger: b.logger, - generic: generic, - networkClassDao: networkClassDao, - subnetDao: subnetDao, - securityGroupDao: securityGroupDao, + logger: b.logger, + generic: generic, + networkClassDao: networkClassDao, } return } @@ -210,62 +186,12 @@ func (s *PrivateVirtualNetworksServer) Update(ctx context.Context, return } -// Delete removes a VirtualNetwork after verifying no child Subnets or SecurityGroups reference it. -// Returns FailedPrecondition when references exist. func (s *PrivateVirtualNetworksServer) Delete(ctx context.Context, request *privatev1.VirtualNetworksDeleteRequest) (response *privatev1.VirtualNetworksDeleteResponse, err error) { - if err = s.checkNoChildReferences(ctx, request.GetId()); err != nil { - return - } - err = s.generic.Delete(ctx, request, &response) return } -// checkNoChildReferences returns nil when no Subnets or SecurityGroups reference virtualNetworkID. -// Returns FailedPrecondition when child resources exist, or Internal when the reference query fails. -func (s *PrivateVirtualNetworksServer) checkNoChildReferences(ctx context.Context, virtualNetworkID string) error { - if virtualNetworkID == "" { - return nil - } - - filter := fmt.Sprintf("this.spec.virtual_network == %q", virtualNetworkID) - - subnetResponse, err := s.subnetDao.List(). - SetFilter(filter). - SetLimit(1). - Do(ctx) - if err != nil { - s.logger.ErrorContext(ctx, "Failed to check subnet references", - slog.String("virtual_network_id", virtualNetworkID), - slog.Any("error", err)) - return grpcstatus.Errorf(grpccodes.Internal, "failed to check child references") - } - if subnetResponse.GetTotal() > 0 { - return grpcstatus.Errorf(grpccodes.FailedPrecondition, - "cannot delete VirtualNetwork '%s': %d Subnet(s) still reference it", - virtualNetworkID, subnetResponse.GetTotal()) - } - - sgResponse, err := s.securityGroupDao.List(). - SetFilter(filter). - SetLimit(1). - Do(ctx) - if err != nil { - s.logger.ErrorContext(ctx, "Failed to check security group references", - slog.String("virtual_network_id", virtualNetworkID), - slog.Any("error", err)) - return grpcstatus.Errorf(grpccodes.Internal, "failed to check child references") - } - if sgResponse.GetTotal() > 0 { - return grpcstatus.Errorf(grpccodes.FailedPrecondition, - "cannot delete VirtualNetwork '%s': %d SecurityGroup(s) still reference it", - virtualNetworkID, sgResponse.GetTotal()) - } - - return nil -} - func (s *PrivateVirtualNetworksServer) Signal(ctx context.Context, request *privatev1.VirtualNetworksSignalRequest) (response *privatev1.VirtualNetworksSignalResponse, err error) { err = s.generic.Signal(ctx, request, &response) diff --git a/internal/servers/private_virtual_networks_server_test.go b/internal/servers/private_virtual_networks_server_test.go index 223c2c789..bbefee118 100644 --- a/internal/servers/private_virtual_networks_server_test.go +++ b/internal/servers/private_virtual_networks_server_test.go @@ -1364,7 +1364,7 @@ var _ = Describe("Private virtual networks server", func() { }) }) - Describe("Deletion referential integrity", func() { + Describe("Deletion validation", func() { var ( vnServer *PrivateVirtualNetworksServer subnetDao *dao.GenericDAO[*privatev1.Subnet] @@ -1536,11 +1536,6 @@ var _ = Describe("Private virtual networks server", func() { sgResp, err := sgDao.Create().SetObject(sg).Do(ctx) Expect(err).ToNot(HaveOccurred()) - _, err = vnServer.Delete(ctx, privatev1.VirtualNetworksDeleteRequest_builder{ - Id: vn.GetId(), - }.Build()) - Expect(err).To(HaveOccurred()) - _, err = sgDao.Delete().SetId(sgResp.GetObject().GetId()).Do(ctx) Expect(err).ToNot(HaveOccurred()) From 44555fc48eae85430fa11831681fb69a397f3cfd Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Mon, 15 Jun 2026 14:22:08 -0500 Subject: [PATCH 5/7] OSAC-463: Create parent VirtualNetwork in compute instance test fixtures Migration 54's subnet insert trigger requires the referenced VirtualNetwork to exist. Seed test-vnet in BeforeEach before creating test-subnet. --- .../servers/compute_instances_server_test.go | 18 +++++++++++++++++- .../private_compute_instances_server_test.go | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/internal/servers/compute_instances_server_test.go b/internal/servers/compute_instances_server_test.go index 66708d64f..6f3eb1ad7 100644 --- a/internal/servers/compute_instances_server_test.go +++ b/internal/servers/compute_instances_server_test.go @@ -87,7 +87,23 @@ var _ = Describe("Compute instances server", func() { Build() Expect(err).ToNot(HaveOccurred()) - // Create a test subnet for all tests to use: + // Create a test virtual network and subnet for all tests to use: + vnDao, err := dao.NewGenericDAO[*privatev1.VirtualNetwork](). + SetLogger(logger). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + + vn := privatev1.VirtualNetwork_builder{ + Id: "test-vnet", + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + }.Build(), + }.Build() + + _, err = vnDao.Create().SetObject(vn).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + subnetsDao, err := dao.NewGenericDAO[*privatev1.Subnet](). SetLogger(logger). SetTenancyLogic(tenancy). diff --git a/internal/servers/private_compute_instances_server_test.go b/internal/servers/private_compute_instances_server_test.go index d3c09a046..811775328 100644 --- a/internal/servers/private_compute_instances_server_test.go +++ b/internal/servers/private_compute_instances_server_test.go @@ -36,7 +36,23 @@ var _ = Describe("Private compute instances server", func() { BeforeEach(func() { var err error - // Create a default test subnet for tests that don't explicitly create one: + // Create a default test virtual network and subnet for tests that don't explicitly create one: + vnDao, err := dao.NewGenericDAO[*privatev1.VirtualNetwork](). + SetLogger(logger). + SetTenancyLogic(tenancy). + Build() + Expect(err).ToNot(HaveOccurred()) + + vn := privatev1.VirtualNetwork_builder{ + Id: "test-vnet", + Metadata: privatev1.Metadata_builder{ + Tenant: auth.SharedTenant, + }.Build(), + }.Build() + + _, err = vnDao.Create().SetObject(vn).Do(ctx) + Expect(err).ToNot(HaveOccurred()) + subnetsDao, err := dao.NewGenericDAO[*privatev1.Subnet](). SetLogger(logger). SetTenancyLogic(tenancy). From c5a48e069aee96e2f6372c878382721f824b3af5 Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Mon, 15 Jun 2026 14:29:29 -0500 Subject: [PATCH 6/7] =?UTF-8?q?OSAC-463:=20Address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20harden=20migration=2054=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assert index expression/predicate via pg_indexes.indexdef and add SecurityGroup non-existent VirtualNetwork insert test. --- ...virtual_network_child_ref_triggers_test.go | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go b/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go index a32ff38f4..82f555948 100644 --- a/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go +++ b/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go @@ -74,18 +74,24 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { err := tool.Migrate(ctx, 54) Expect(err).ToNot(HaveOccurred()) - for _, indexName := range []string{"subnets_by_virtual_network", "security_groups_by_virtual_network"} { - var count int + type idxExpectation struct { + name string + table string + } + for _, tc := range []idxExpectation{ + {name: "subnets_by_virtual_network", table: "subnets"}, + {name: "security_groups_by_virtual_network", table: "security_groups"}, + } { + var indexDef string err = conn.QueryRow(ctx, ` - select - count(*) - from - pg_indexes - where - indexname = $1 - `, indexName).Scan(&count) + select indexdef + from pg_indexes + where indexname = $1 + and tablename = $2 + `, tc.name, tc.table).Scan(&indexDef) Expect(err).ToNot(HaveOccurred()) - Expect(count).To(Equal(1)) + Expect(indexDef).To(ContainSubstring("(data -> 'spec'::text) ->> 'virtual_network'::text")) + Expect(indexDef).To(ContainSubstring("WHERE (deletion_timestamp = '1970-01-01 00:00:00+00'::timestamp with time zone)")) } }) @@ -289,6 +295,22 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { Expect(count).To(Equal(1)) }) + It("Prevents creating a security group referencing a non-existent virtual network", func(ctx context.Context) { + err := tool.Migrate(ctx, 54) + Expect(err).ToNot(HaveOccurred()) + insertTenant(ctx) + + _, err = conn.Exec(ctx, + `insert into security_groups (id, tenant, data) + values ('sg-ref-missing', 'test-tenant', $1::jsonb)`, + `{"spec":{"virtual_network":"no-such-vn"}}`) + Expect(err).To(HaveOccurred()) + var pgErr *pgconn.PgError + Expect(errors.As(err, &pgErr)).To(BeTrue()) + Expect(pgErr.Code).To(Equal("Z0002")) + Expect(pgErr.Message).To(ContainSubstring("no-such-vn")) + }) + It("Prevents creating a security group referencing a soft-deleted virtual network", func(ctx context.Context) { err := tool.Migrate(ctx, 54) Expect(err).ToNot(HaveOccurred()) From e3a93195da16e7d0a8b5d4a99a646d0e92eec0ed Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Mon, 15 Jun 2026 21:18:04 -0500 Subject: [PATCH 7/7] OSAC-463: Renumber VN child-ref migration to 55 after main took 54 main added 54_create_baremetal_tables; duplicate migration 54 broke CI BeforeSuite. Renumber OSAC-463 triggers to migration 55. --- ...virtual_network_child_ref_triggers.up.sql} | 0 ...irtual_network_child_ref_triggers_test.go} | 28 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) rename internal/database/migrations/{54_add_virtual_network_child_ref_triggers.up.sql => 55_add_virtual_network_child_ref_triggers.up.sql} (100%) rename internal/database/migrations/{54_add_virtual_network_child_ref_triggers_test.go => 55_add_virtual_network_child_ref_triggers_test.go} (96%) diff --git a/internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql b/internal/database/migrations/55_add_virtual_network_child_ref_triggers.up.sql similarity index 100% rename from internal/database/migrations/54_add_virtual_network_child_ref_triggers.up.sql rename to internal/database/migrations/55_add_virtual_network_child_ref_triggers.up.sql diff --git a/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go b/internal/database/migrations/55_add_virtual_network_child_ref_triggers_test.go similarity index 96% rename from internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go rename to internal/database/migrations/55_add_virtual_network_child_ref_triggers_test.go index 82f555948..757e84afd 100644 --- a/internal/database/migrations/54_add_virtual_network_child_ref_triggers_test.go +++ b/internal/database/migrations/55_add_virtual_network_child_ref_triggers_test.go @@ -33,7 +33,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { } It("Creates the 'check_virtual_network_not_in_use' function", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) var count int @@ -51,7 +51,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Adds a delete trigger to the virtual_networks table", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) var count int @@ -71,7 +71,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Creates indexes for child virtual_network lookups", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) type idxExpectation struct { @@ -96,7 +96,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents soft-deleting a virtual network referenced by a subnet", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -122,7 +122,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents soft-deleting a virtual network referenced by a security group", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -148,7 +148,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Reports the count of referencing subnets", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -176,7 +176,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Allows soft-deleting a virtual network with no child references", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -191,7 +191,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Allows soft-deleting a virtual network when child subnets are already deleted", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -216,7 +216,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Creates the subnet virtual_network insert trigger", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) var count int @@ -236,7 +236,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents creating a subnet referencing a non-existent virtual network", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -252,7 +252,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents creating a subnet referencing a soft-deleted virtual network", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -276,7 +276,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Creates the security group virtual_network insert trigger", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) var count int @@ -296,7 +296,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents creating a security group referencing a non-existent virtual network", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx) @@ -312,7 +312,7 @@ var _ = DescribeMigration("Add virtual network child ref triggers", func() { }) It("Prevents creating a security group referencing a soft-deleted virtual network", func(ctx context.Context) { - err := tool.Migrate(ctx, 54) + err := tool.Migrate(ctx, 55) Expect(err).ToNot(HaveOccurred()) insertTenant(ctx)