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
30 changes: 22 additions & 8 deletions docs/api-types/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,28 @@ Service networks are identified by Gateway name (without namespace) - for exampl
will point to a VPC Lattice service network `my-gateway`. If multiple Gateways share the same name, all of them
will point to the same service network.

VPC Lattice service networks must be managed separately, as it is a broader concept that can cover resources
outside the Kubernetes cluster. To create and manage a service network, you can either:

- Specify `DEFAULT_SERVICE_NETWORK` configuration option on the controller. This will make the controller
to create a service network with such name, and associate the cluster VPC to it for you. This is suitable
for simple use cases with single service network.
- Manage service networks outside the cluster, using AWS Console, CDK, CloudFormation, etc. This is recommended
for more advanced use cases that cover multiple clusters and VPCs.
### ServiceNetwork Lifecycle

When a Gateway is created, the controller automatically creates a VPC Lattice ServiceNetwork with the same name
and associates it with the cluster VPC. When the Gateway is deleted, the controller deletes the ServiceNetwork
if it was created by the controller (tracked via the `application-networking.k8s.aws/ManagedBy` tag).

Auto-created ServiceNetworks use **default VPC Lattice settings** — no auth policy, no sharing configuration,
and no custom attributes. If you need to configure auth type, sharing, or other
[ServiceNetwork attributes](https://docs.aws.amazon.com/vpc-lattice/latest/APIReference/API_CreateServiceNetwork.html),
create the ServiceNetwork externally (via Console, CLI, CDK, CloudFormation, etc.) before creating the Gateway.
The controller will detect the existing ServiceNetwork by name and reuse it without modifying or deleting it.

**Deletion behavior:**

- If multiple Gateways share the same ServiceNetwork name, deleting one Gateway will **not** delete the
ServiceNetwork as long as another active Gateway with the same name still exists.
- If the ServiceNetwork has active service associations, the controller will not delete it and will report
an error asking you to detach all services first.
- Externally-created ServiceNetworks are never deleted by the controller.

In addition to auto-creation, you can also use the `DEFAULT_SERVICE_NETWORK` configuration option on the controller
to create a default ServiceNetwork at startup.

Gateways with `amazon-vpc-lattice` GatewayClass do not create a single entrypoint to bind Listeners and Routes
under them. Instead, each Route will have its own domain name assigned. To see an example of how domain names
Expand Down
72 changes: 61 additions & 11 deletions pkg/controllers/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type gatewayReconciler struct {
finalizerManager k8s.FinalizerManager
eventRecorder record.EventRecorder
cloud aws.Cloud
snManager deploy.ServiceNetworkManager
}

func RegisterGatewayController(
Expand All @@ -73,18 +74,20 @@ func RegisterGatewayController(
scheme := mgr.GetScheme()
evtRec := mgr.GetEventRecorderFor("gateway")

snManager := deploy.NewDefaultServiceNetworkManager(log, cloud)

r := &gatewayReconciler{
log: log,
client: mgrClient,
scheme: scheme,
finalizerManager: finalizerManager,
eventRecorder: evtRec,
cloud: cloud,
snManager: snManager,
}

if config.DefaultServiceNetwork != "" {
// Attempt creation of default service network, move gracefully even if it fails.
snManager := deploy.NewDefaultServiceNetworkManager(log, cloud)
_, err := snManager.CreateOrUpdate(context.Background(), &model.ServiceNetwork{
Spec: model.ServiceNetworkSpec{
Name: config.DefaultServiceNetwork,
Expand Down Expand Up @@ -177,12 +180,60 @@ func (r *gatewayReconciler) reconcileDelete(ctx context.Context, gw *gwv1.Gatewa
}
}

err = r.finalizerManager.RemoveFinalizers(ctx, gw, gatewayFinalizer)
hasSibling, err := r.hasSiblingGateway(ctx, gw)
if err != nil {
return err
}
if hasSibling {
r.log.Infof(ctx, "Skipping ServiceNetwork deletion for %s, another Gateway with the same name exists", gw.Name)
} else {
if err := r.snManager.Delete(ctx, gw.Name); err != nil {
return err
}
}

return nil
return r.finalizerManager.RemoveFinalizers(ctx, gw, gatewayFinalizer)
}

// hasSiblingGateway checks if another active Lattice-controlled Gateway with the same .Name exists.
// Gateways that are themselves being deleted (DeletionTimestamp set) are not counted as siblings,
// so that simultaneous deletion of all Gateways sharing an SN name still cleans up the SN.
func (r *gatewayReconciler) hasSiblingGateway(ctx context.Context, gw *gwv1.Gateway) (bool, error) {
gwList := &gwv1.GatewayList{}
if err := r.client.List(ctx, gwList); err != nil {
return false, fmt.Errorf("failed to list gateways: %w", err)
}
for i := range gwList.Items {
other := &gwList.Items[i]
if other.UID == gw.UID {
continue
}
if other.Name == gw.Name && other.DeletionTimestamp.IsZero() &&
k8s.IsControlledByLatticeGatewayController(ctx, r.client, other) {
return true, nil
}
}
return false, nil
}

// shouldAssociateWithVpc returns true unless a VpcAssociationPolicy targeting this Gateway
// explicitly sets associateWithVpc to false. This prevents the Gateway reconciler from
// re-creating a VPC association that the VpcAssociationPolicy controller intentionally deleted.
func (r *gatewayReconciler) shouldAssociateWithVpc(ctx context.Context, gw *gwv1.Gateway) bool {
vapList := &anv1alpha1.VpcAssociationPolicyList{}
if err := r.client.List(ctx, vapList, client.InNamespace(gw.Namespace)); err != nil {
return true
}
for i := range vapList.Items {
vap := &vapList.Items[i]
if string(vap.Spec.TargetRef.Name) == gw.Name {
if vap.Spec.AssociateWithVpc != nil && !*vap.Spec.AssociateWithVpc {
return false
}
return true
}
}
return true
}

func (r *gatewayReconciler) reconcileUpsert(ctx context.Context, gw *gwv1.Gateway) error {
Expand All @@ -205,14 +256,13 @@ func (r *gatewayReconciler) reconcileUpsert(ctx context.Context, gw *gwv1.Gatewa
return err
}

snInfo, err := r.cloud.Lattice().FindServiceNetwork(ctx, gw.Name)
snStatus, err := r.snManager.CreateOrUpdate(ctx, &model.ServiceNetwork{
Spec: model.ServiceNetworkSpec{
Name: gw.Name,
AssociateToVPC: r.shouldAssociateWithVpc(ctx, gw),
},
})
if err != nil {
if services.IsNotFoundError(err) {
if err = r.updateGatewayProgrammedStatus(ctx, gw, gwv1.GatewayReasonPending, "VPC Lattice Service Network not found"); err != nil {
return lattice_runtime.NewRetryError()
}
return nil
}
if errors.Is(err, services.ErrNameConflict) {
if err = r.updateGatewayProgrammedStatus(ctx, gw, gwv1.GatewayReasonInvalid, "Found multiple VPC Lattice Service Networks matching Gateway name. Either ensure only one Service Network has a matching name, or use the Service Network's id as the Gateway name."); err != nil {
return lattice_runtime.NewRetryError()
Expand All @@ -222,7 +272,7 @@ func (r *gatewayReconciler) reconcileUpsert(ctx context.Context, gw *gwv1.Gatewa
return err
}

err = r.updateGatewayProgrammedStatus(ctx, gw, gwv1.GatewayReasonProgrammed, fmt.Sprintf("aws-service-network-arn: %s", *snInfo.SvcNetwork.Arn))
err = r.updateGatewayProgrammedStatus(ctx, gw, gwv1.GatewayReasonProgrammed, fmt.Sprintf("aws-service-network-arn: %s", snStatus.ServiceNetworkARN))
if err != nil {
return err
}
Expand Down
76 changes: 76 additions & 0 deletions pkg/controllers/gateway_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,79 @@ func TestUpdateGWListenerStatus_SupportedKinds(t *testing.T) {
})
}
}

func TestHasSiblingGateway(t *testing.T) {
scheme := runtime.NewScheme()
clientgoscheme.AddToScheme(scheme)
gwv1.Install(scheme)
gwv1alpha2.Install(scheme)
addOptionalCRDs(scheme)

latticeControllerName := gwv1.GatewayController("application-networking.k8s.aws/gateway-api-controller")
gwClass := &gwv1.GatewayClass{
ObjectMeta: metav1.ObjectMeta{Name: "amazon-vpc-lattice"},
Spec: gwv1.GatewayClassSpec{ControllerName: latticeControllerName},
}

makeGW := func(name, ns string, uid types.UID, deleting bool) *gwv1.Gateway {
gw := &gwv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: name, Namespace: ns, UID: uid,
},
Spec: gwv1.GatewaySpec{GatewayClassName: "amazon-vpc-lattice"},
}
if deleting {
now := metav1.Now()
gw.DeletionTimestamp = &now
gw.Finalizers = []string{"test"}
}
return gw
}

tests := []struct {
name string
gw *gwv1.Gateway
others []*gwv1.Gateway
expected bool
}{
{
name: "no sibling — only gateway with this name",
gw: makeGW("my-network", "ns1", "uid-1", false),
others: nil,
expected: false,
},
{
name: "has sibling — same name different namespace",
gw: makeGW("my-network", "ns1", "uid-1", false),
others: []*gwv1.Gateway{makeGW("my-network", "ns2", "uid-2", false)},
expected: true,
},
{
name: "sibling being deleted — not counted",
gw: makeGW("my-network", "ns1", "uid-1", false),
others: []*gwv1.Gateway{makeGW("my-network", "ns2", "uid-2", true)},
expected: false,
},
{
name: "different name — not a sibling",
gw: makeGW("my-network", "ns1", "uid-1", false),
others: []*gwv1.Gateway{makeGW("other-network", "ns2", "uid-2", false)},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objs := []runtime.Object{gwClass, tt.gw}
for _, o := range tt.others {
objs = append(objs, o)
}
k8sClient := testclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()

r := &gatewayReconciler{client: k8sClient}
result, err := r.hasSiblingGateway(context.Background(), tt.gw)
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
52 changes: 52 additions & 0 deletions pkg/deploy/lattice/service_network_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ServiceNetworkManager interface {
DeleteVpcAssociation(ctx context.Context, snName string) error

CreateOrUpdate(ctx context.Context, serviceNetwork *model.ServiceNetwork) (model.ServiceNetworkStatus, error)
Delete(ctx context.Context, snName string) error
}

func NewDefaultServiceNetworkManager(log gwlog.Logger, cloud pkg_aws.Cloud) *defaultServiceNetworkManager {
Expand Down Expand Up @@ -172,6 +173,52 @@ func (m *defaultServiceNetworkManager) DeleteVpcAssociation(ctx context.Context,
return nil
}

func (m *defaultServiceNetworkManager) Delete(ctx context.Context, snName string) error {
sn, err := m.cloud.Lattice().FindServiceNetwork(ctx, snName)
if err != nil {
if services.IsNotFoundError(err) {
return nil
}
return err
}

snArn := aws.StringValue(sn.SvcNetwork.Arn)
owned, err := m.cloud.IsArnManaged(ctx, snArn)
if err != nil {
m.log.Warnf(ctx, "cannot check ownership of ServiceNetwork %s: %s, skipping deletion", snName, err)
return nil
}
if !owned {
m.log.Infof(ctx, "ServiceNetwork %s not owned by controller, skipping deletion", snName)
return nil
}

assocs, err := m.cloud.Lattice().ListServiceNetworkServiceAssociationsAsList(ctx,
&vpclattice.ListServiceNetworkServiceAssociationsInput{
ServiceNetworkIdentifier: sn.SvcNetwork.Id,
})
if err != nil {
return fmt.Errorf("failed to list service associations for ServiceNetwork %s: %w", snName, err)
}
if len(assocs) > 0 {
return fmt.Errorf("cannot delete ServiceNetwork %s: %d service association(s) still active, "+
"detach all services before deleting the Gateway", snName, len(assocs))
}

if err := m.DeleteVpcAssociation(ctx, snName); err != nil {
return err
}

_, err = m.cloud.Lattice().DeleteServiceNetworkWithContext(ctx, &vpclattice.DeleteServiceNetworkInput{
ServiceNetworkIdentifier: sn.SvcNetwork.Id,
})
if err != nil {
return err
}
m.log.Infof(ctx, "Deleted ServiceNetwork %s", snName)
return nil
}

func (m *defaultServiceNetworkManager) getActiveVpcAssociation(ctx context.Context, serviceNetworkId string) (*vpclattice.ServiceNetworkVpcAssociationSummary, error) {
vpcLatticeSess := m.cloud.Lattice()
associationStatusInput := vpclattice.ListServiceNetworkVpcAssociationsInput{
Expand Down Expand Up @@ -252,6 +299,11 @@ func (m *defaultServiceNetworkManager) CreateOrUpdate(ctx context.Context, servi
}
}

if !serviceNetwork.Spec.AssociateToVPC {
m.log.Debugf(ctx, "Skipping VPC association for ServiceNetwork %s (AssociateToVPC=false)", serviceNetwork.Spec.Name)
return model.ServiceNetworkStatus{ServiceNetworkARN: serviceNetworkArn, ServiceNetworkID: serviceNetworkId}, nil
}

m.log.Debugf(ctx, "Creating association between ServiceNetwork %s and VPC %s", serviceNetworkId, config.VpcID)
createServiceNetworkVpcAssociationInput := vpclattice.CreateServiceNetworkVpcAssociationInput{
ServiceNetworkIdentifier: &serviceNetworkId,
Expand Down
14 changes: 14 additions & 0 deletions pkg/deploy/lattice/service_network_manager_mock.go

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

Loading
Loading