Skip to content
Merged
13 changes: 13 additions & 0 deletions api/v1alpha2/linodevpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ type LinodeVPCSpec struct {
// +optional
Subnets []VPCSubnetCreateOptions `json:"subnets,omitempty"`

// Retain allows you to keep the VPC after the LinodeVPC object is deleted.
// This is useful if you want to use an existing VPC that was not created by this controller.
// If set to true, the controller will not delete the VPC resource in Linode.
// Defaults to false.
// +optional
// +kubebuilder:default=false
Retain bool `json:"retain,omitempty"`

// CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this VPC. If not
// supplied then the credentials of the controller will be used.
// +optional
Expand All @@ -55,6 +63,11 @@ type VPCSubnetCreateOptions struct {
// SubnetID is subnet id for the subnet
// +optional
SubnetID int `json:"subnetID,omitempty"`
// Retain allows you to keep the Subnet after the LinodeVPC object is deleted.
// This is only applicable when the parent VPC has retain set to true.
// +optional
// +kubebuilder:default=false
Retain bool `json:"retain,omitempty"`
}

// LinodeVPCStatus defines the observed state of LinodeVPC
Expand Down
2 changes: 2 additions & 0 deletions clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type LinodeVPCClient interface {
ListVPCs(ctx context.Context, opts *linodego.ListOptions) ([]linodego.VPC, error)
CreateVPC(ctx context.Context, opts linodego.VPCCreateOptions) (*linodego.VPC, error)
DeleteVPC(ctx context.Context, vpcID int) error
CreateVPCSubnet(ctx context.Context, opts linodego.VPCSubnetCreateOptions, vpcID int) (*linodego.VPCSubnet, error)
DeleteVPCSubnet(ctx context.Context, vpcID, subnetID int) error
}

// LinodeNodeBalancerClient defines the methods that interact with Linode's Node Balancer service.
Expand Down
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@
if err := (&controller.LinodeVPCReconciler{
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor("LinodeVPCReconciler"),
WatchFilterValue: flags.clusterWatchFilter,
LinodeClientConfig: linodeClientConfig,
WatchFilterValue: flags.clusterWatchFilter,

Check warning on line 300 in cmd/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/main.go#L300

Added line #L300 was not covered by tests
}).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: flags.linodeVPCConcurrency}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "LinodeVPC")
os.Exit(1)
Expand Down
14 changes: 14 additions & 0 deletions config/crd/bases/infrastructure.cluster.x-k8s.io_linodevpcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ spec:
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
retain:
default: false
description: |-
Retain allows you to keep the VPC after the LinodeVPC object is deleted.
This is useful if you want to use an existing VPC that was not created by this controller.
If set to true, the controller will not delete the VPC resource in Linode.
Defaults to false.
type: boolean
subnets:
items:
description: VPCSubnetCreateOptions defines subnet options
Expand All @@ -82,6 +90,12 @@ spec:
maxLength: 63
minLength: 3
type: string
retain:
default: false
description: |-
Retain allows you to keep the Subnet after the LinodeVPC object is deleted.
This is only applicable when the parent VPC has retain set to true.
type: boolean
subnetID:
description: SubnetID is subnet id for the subnet
type: integer
Expand Down
2 changes: 2 additions & 0 deletions docs/src/reference/out.md
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ _Appears in:_
| `description` _string_ | | | |
| `region` _string_ | | | |
| `subnets` _[VPCSubnetCreateOptions](#vpcsubnetcreateoptions) array_ | | | |
| `retain` _boolean_ | Retain allows you to keep the VPC after the LinodeVPC object is deleted.<br />This is useful if you want to use an existing VPC that was not created by this controller.<br />If set to true, the controller will not delete the VPC resource in Linode.<br />Defaults to false. | false | |
| `credentialsRef` _[SecretReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#secretreference-v1-core)_ | CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this VPC. If not<br />supplied then the credentials of the controller will be used. | | |


Expand Down Expand Up @@ -1237,5 +1238,6 @@ _Appears in:_
| `label` _string_ | | | MaxLength: 63 <br />MinLength: 3 <br /> |
| `ipv4` _string_ | | | |
| `subnetID` _integer_ | SubnetID is subnet id for the subnet | | |
| `retain` _boolean_ | Retain allows you to keep the Subnet after the LinodeVPC object is deleted.<br />This is only applicable when the parent VPC has retain set to true. | false | |


38 changes: 38 additions & 0 deletions docs/src/topics/vpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ spec:

Reference to LinodeVPC object is added to LinodeCluster object which then uses the specified VPC to provision resources.

## Lifecycle Management and Adopting Existing VPCs

The provider offers flexible lifecycle management, allowing you to adopt existing VPCs and control whether resources are deleted when their corresponding Kubernetes objects are removed.

### Adopting an Existing VPC
You can instruct the controller to use a pre-existing VPC by specifying its ID in the `LinodeVPCSpec`. The controller will "adopt" this VPC and manage its subnets without creating a new one.

```yaml
---
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2
kind: LinodeVPC
metadata:
name: my-adopted-vpc
spec:
vpcID: 12345
region: us-sea
# subnets can be defined and will be created within the adopted VPC
subnets:
- label: my-new-subnet-in-adopted-vpc
ipv4: 10.0.3.0/24
```

```admonish note
We currently don't have functionality to update predefined/already-created subnets. We only have create/delete operations at the moment.
```

### Retaining Resources on Deletion
By default, the controller deletes VPCs and subnets from your Linode account when you delete the `LinodeVPC` Kubernetes object. You can prevent this using the `retain` flag.

- **`spec.retain`**: When set to `true` on the `LinodeVPC`, the VPC itself will not be deleted from Linode. This is the default and recommended behavior when adopting an existing VPC.
- **`spec.subnets[].retain`**: When the parent VPC is retained, you can use this flag to control individual subnets. If `retain` is `false` (the default), the subnet will be deleted.

```admonish warning title="Safety Check for Attached Linodes"
The controller includes a critical safety feature: it will **not** delete a subnet if it has any active Linode instances attached to it. The operation will be paused and retried, preventing resource orphaning.
```

### Additional Configuration
By default, the VPC will use the subnet with the `default` label for deploying clusters. To modify this behavior, set the `SUBNET_NAME` environment variable to match the label of the subnet to be used. Make sure the subnet is set up in the LinodeVPC manifest.

Expand Down Expand Up @@ -111,3 +147,5 @@ CIDR returned in the output of above command should match with the pod CIDR pres

### Running cilium connectivity tests
One can also run cilium connectivity tests to make sure networking works fine within VPC. Follow the steps defined in [cilium e2e tests](https://docs.cilium.io/en/stable/contributing/testing/e2e/) guide to install cilium binary, set the KUBECONFIG variable and then run `cilium connectivity tests`.

```
Copy link
Collaborator

Choose a reason for hiding this comment

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

accidental addition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops yeah let me remove that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Collaborator

Choose a reason for hiding this comment

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

looks like it came back? 🤔

201 changes: 150 additions & 51 deletions internal/controller/linodevpc_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"time"

"github.com/go-logr/logr"
"github.com/linode/linodego"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -289,85 +290,73 @@
return nil
}

//nolint:nestif,gocognit // As simple as possible.
func (r *LinodeVPCReconciler) reconcileDelete(ctx context.Context, logger logr.Logger, vpcScope *scope.VPCScope) (ctrl.Result, error) {
logger.Info("deleting VPC")

if vpcScope.LinodeVPC.Spec.VPCID != nil {
vpc, err := vpcScope.LinodeClient.GetVPC(ctx, *vpcScope.LinodeVPC.Spec.VPCID)
if util.IgnoreLinodeAPIError(err, http.StatusNotFound) != nil {
logger.Error(err, "Failed to fetch VPC")

if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion")

return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}
if vpcScope.LinodeVPC.Spec.Retain {
return r.handleRetainedVPC(ctx, logger, vpcScope)
}

return ctrl.Result{}, err
if err := r.deleteVPCResources(ctx, logger, vpcScope); err != nil {
if errors.Is(err, util.ErrReconcileAgain) {
return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}
return ctrl.Result{}, err
}

if vpc != nil {
for i := range vpc.Subnets {
if len(vpc.Subnets[i].Linodes) == 0 {
continue
}

logger.Info("VPC subnets still has node(s) attached")

if vpc.Updated.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerWaitForHasNodesTimeout)).After(time.Now()) {
logger.Info("VPC has node(s) attached, re-queuing VPC deletion")

return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}
conditions.Set(vpcScope.LinodeVPC, metav1.Condition{
Type: string(clusterv1.ReadyCondition),
Status: metav1.ConditionFalse,
Reason: string(clusterv1.DeletedReason),
Message: "VPC deleted",
})
r.Recorder.Event(vpcScope.LinodeVPC, corev1.EventTypeNormal, clusterv1.DeletedReason, "VPC has cleaned up")

conditions.Set(vpcScope.LinodeVPC, metav1.Condition{
Type: string(clusterv1.ReadyCondition),
Status: metav1.ConditionFalse,
Reason: string(clusterv1.DeletionFailedReason),
Message: "skipped due to node(s) attached",
})
vpcScope.LinodeVPC.Spec.VPCID = nil

return ctrl.Result{}, errors.New("will not delete VPC with node(s) attached")
}
if err := vpcScope.RemoveCredentialsRefFinalizer(ctx); err != nil {
logger.Error(err, "Failed to update credentials secret")
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion")
return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}
return ctrl.Result{}, err

Check warning on line 323 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L318-L323

Added lines #L318 - L323 were not covered by tests
}

err = vpcScope.LinodeClient.DeleteVPC(ctx, *vpcScope.LinodeVPC.Spec.VPCID)
if util.IgnoreLinodeAPIError(err, http.StatusNotFound) != nil {
logger.Error(err, "Failed to delete VPC")
controllerutil.RemoveFinalizer(vpcScope.LinodeVPC, infrav1alpha2.VPCFinalizer)
// TODO: remove this check and removal later
if controllerutil.ContainsFinalizer(vpcScope.LinodeVPC, infrav1alpha2.GroupVersion.String()) {
controllerutil.RemoveFinalizer(vpcScope.LinodeVPC, infrav1alpha2.GroupVersion.String())
}

Check warning on line 330 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L329-L330

Added lines #L329 - L330 were not covered by tests

if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion")
return ctrl.Result{}, nil
}

return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}
func (r *LinodeVPCReconciler) handleRetainedVPC(ctx context.Context, logger logr.Logger, vpcScope *scope.VPCScope) (ctrl.Result, error) {
logger.Info("VPC has retain flag, skipping VPC deletion")

return ctrl.Result{}, err
}
if err := r.handleRetainedSubnets(ctx, logger, vpcScope); err != nil {
if errors.Is(err, util.ErrReconcileAgain) {
return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil

Check warning on line 340 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L339-L340

Added lines #L339 - L340 were not covered by tests
}
} else {
logger.Info("VPC ID is missing, nothing to do")
return ctrl.Result{}, err

Check warning on line 342 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L342

Added line #L342 was not covered by tests
}

conditions.Set(vpcScope.LinodeVPC, metav1.Condition{
Type: string(clusterv1.ReadyCondition),
Status: metav1.ConditionFalse,
Reason: string(clusterv1.DeletedReason),
Message: "VPC deleted",
Message: "VPC retained as requested, associated cloud resource was not deleted.",
})

r.Recorder.Event(vpcScope.LinodeVPC, corev1.EventTypeNormal, clusterv1.DeletedReason, "VPC has cleaned up")

vpcScope.LinodeVPC.Spec.VPCID = nil
r.Recorder.Event(vpcScope.LinodeVPC, corev1.EventTypeNormal, "VPCResourceRetained", "VPC retained as requested, associated cloud resource was not deleted.")

if err := vpcScope.RemoveCredentialsRefFinalizer(ctx); err != nil {
logger.Error(err, "Failed to update credentials secret")

if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion")

return ctrl.Result{RequeueAfter: reconciler.DefaultVPCControllerReconcileDelay}, nil
}

return ctrl.Result{}, err
}

Expand All @@ -380,6 +369,116 @@
return ctrl.Result{}, nil
}

func (r *LinodeVPCReconciler) handleRetainedSubnets(ctx context.Context, logger logr.Logger, vpcScope *scope.VPCScope) error {
vpc, err := getVPC(ctx, vpcScope)
if err != nil {
if errors.Is(err, ErrVPCNotFound) {
return nil
}
logger.Error(err, "Failed to fetch VPC for subnet deletion")
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion due to fetch error for subnet deletion")
return util.ErrReconcileAgain
}
return err

Check warning on line 383 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L375-L383

Added lines #L375 - L383 were not covered by tests
}

// index the subnets by ID for quick lookup
apiSubnets := make(map[int]linodego.VPCSubnet)
for _, s := range vpc.Subnets {
apiSubnets[s.ID] = s
}

for _, subnet := range vpcScope.LinodeVPC.Spec.Subnets {
if subnet.Retain {
continue
}

if apiSubnet, ok := apiSubnets[subnet.SubnetID]; ok {
if len(apiSubnet.Linodes) > 0 {
logger.Info("subnet still has node(s) attached", "subnetID", subnet.SubnetID)
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerWaitForHasNodesTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion due to attached nodes in subnet")
return util.ErrReconcileAgain
}

Check warning on line 403 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L399-L403

Added lines #L399 - L403 were not covered by tests

conditions.Set(vpcScope.LinodeVPC, metav1.Condition{
Type: string(clusterv1.ReadyCondition),
Status: metav1.ConditionFalse,
Reason: string(clusterv1.DeletionFailedReason),
Message: fmt.Sprintf("will not delete subnet %d with node(s) attached", subnet.SubnetID),
})
return fmt.Errorf("will not delete subnet %d with node(s) attached", subnet.SubnetID)

Check warning on line 411 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L405-L411

Added lines #L405 - L411 were not covered by tests
}
}

logger.Info("deleting subnet", "subnetID", subnet.SubnetID)
deleteErr := vpcScope.LinodeClient.DeleteVPCSubnet(ctx, *vpcScope.LinodeVPC.Spec.VPCID, subnet.SubnetID)
if deleteErr != nil {
if err := util.IgnoreLinodeAPIError(deleteErr, http.StatusNotFound); err != nil {
logger.Error(err, "Failed to delete subnet")
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion due to subnet delete error")
return util.ErrReconcileAgain
}
return err

Check warning on line 424 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L418-L424

Added lines #L418 - L424 were not covered by tests
}
}
}

return nil
}

func (r *LinodeVPCReconciler) deleteVPCResources(ctx context.Context, logger logr.Logger, vpcScope *scope.VPCScope) error {
vpc, err := getVPC(ctx, vpcScope)
if err != nil {
if errors.Is(err, ErrVPCNotFound) {
logger.Info("VPC not found, nothing to do")
return nil
}

Check warning on line 438 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L436-L438

Added lines #L436 - L438 were not covered by tests
logger.Error(err, "Failed to fetch VPC")
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion due to fetch error")
return util.ErrReconcileAgain
}
return err
}

for i := range vpc.Subnets {
if len(vpc.Subnets[i].Linodes) == 0 {
continue
}

logger.Info("VPC subnets still has node(s) attached")
if vpc.Updated.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerWaitForHasNodesTimeout)).After(time.Now()) {
logger.Info("VPC has node(s) attached, re-queuing VPC deletion")
return util.ErrReconcileAgain
}

conditions.Set(vpcScope.LinodeVPC, metav1.Condition{
Type: string(clusterv1.ReadyCondition),
Status: metav1.ConditionFalse,
Reason: string(clusterv1.DeletionFailedReason),
Message: "skipped due to node(s) attached",
})
return errors.New("will not delete VPC with node(s) attached")
}

if err := vpcScope.LinodeClient.DeleteVPC(ctx, *vpcScope.LinodeVPC.Spec.VPCID); err != nil {
if util.IgnoreLinodeAPIError(err, http.StatusNotFound) == nil {
return nil
}

Check warning on line 470 in internal/controller/linodevpc_controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/linodevpc_controller.go#L469-L470

Added lines #L469 - L470 were not covered by tests
logger.Error(err, "Failed to delete VPC")
if vpcScope.LinodeVPC.ObjectMeta.DeletionTimestamp.Add(reconciler.DefaultTimeout(r.ReconcileTimeout, reconciler.DefaultVPCControllerReconcileTimeout)).After(time.Now()) {
logger.Info("re-queuing VPC deletion due to delete error")
return util.ErrReconcileAgain
}
return err
}

return nil
}

// SetupWithManager sets up the controller with the Manager.
//
//nolint:dupl // this is same as Placement Group, worth making generic later.
Expand Down
Loading
Loading