Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
TAG: e2e-weekly-${{ github.sha }}
run: |
make test-e2e \
GINKGO_LABEL_FILTER="ha || upgrade || self-hosted || kcp-remediation || topology || conformance || pre-existing-networking" \
GINKGO_LABEL_FILTER="ha || upgrade || self-hosted || kcp-remediation || topology || conformance || pre-existing-networking || router-nat" \
KUBETEST_CONFIGURATION=./data/kubetest/conformance-fast.yaml

- name: Install regctl
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- test-e2e-self-hosted
- test-e2e-md-remediation
- test-e2e-pre-existing-networking
- test-e2e-router-nat
- test-e2e-topology
- test-e2e-upgrade
- test-e2e-conformance
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ go.work
_artifacts/
test/e2e/config/*.generated.yaml
test/e2e/data/infrastructure-cloudscale/main/cluster-template*.yaml
test/e2e/data/infrastructure-cloudscale/main/clusterclass*.yaml

# release
dist/
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ For the prose architecture sketch see [`docs/development.md`](docs/development.m
- The e2e overlay `test/infrastructure/cloudscale/clusterclass-quick-start/cluster-class.yaml`
intentionally duplicates `templates/cluster-class.yaml`. Keep them in sync,
but do not attempt to merge or deduplicate them — both copies are required.
The same applies to `clusterclass-router-nat/cluster-class.yaml` and
`templates/cluster-class-router-nat.yaml`; those two are byte-identical.

## Cloudscale SDK usage

- Do not `import "github.com/cloudscale-ch/cloudscale-go-sdk/v9"` outside
- Do not `import "github.com/cloudscale-ch/cloudscale-go-sdk/v10"` outside
`internal/cloudscale/`. Controllers and webhooks talk to the SDK through
the service interfaces on `cloudscale.Client`
(`internal/cloudscale/client.go:32`).
Expand Down
18 changes: 17 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,12 @@ E2E_CLUSTER_TEMPLATES := cluster-template \
cluster-template-md-remediation \
cluster-template-pre-existing-network \
cluster-template-public-lb-private-nodes \
cluster-template-router-nat \
cluster-template-topology \
cluster-template-topology-router-nat \
cluster-template-upgrades \
clusterclass-quick-start
clusterclass-quick-start \
clusterclass-router-nat

.PHONY: generate-e2e-templates
generate-e2e-templates: $(KUSTOMIZE) generate-e2e-cni generate-e2e-ccm ## Generate e2e cluster templates using kustomize overlays
Expand Down Expand Up @@ -285,6 +288,19 @@ test-e2e-topology: $(GINKGO) generate-e2e-templates generate-e2e-config docker-b
-e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) \
-e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER)

.PHONY: test-e2e-router-nat
test-e2e-router-nat: $(GINKGO) generate-e2e-templates generate-e2e-config docker-build ## Run managed-router NAT e2e tests
$(GINKGO) -v --trace --tags=e2e \
--nodes=$(GINKGO_NODES) \
--label-filter="router-nat" \
--timeout=90m \
--output-dir="$(E2E_ARTIFACTS_FOLDER)" --junit-report="junit.e2e_router_nat.xml" \
./test/e2e -- \
-e2e.config=$(E2E_CONF_FILE) \
-e2e.artifacts-folder=$(E2E_ARTIFACTS_FOLDER) \
-e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) \
-e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER)

.PHONY: test-e2e-conformance
test-e2e-conformance: $(GINKGO) generate-e2e-templates generate-e2e-config docker-build ## Run K8s conformance e2e tests
$(GINKGO) -v --trace --tags=e2e \
Expand Down
150 changes: 143 additions & 7 deletions api/v1beta2/cloudscalecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ type CloudscaleClusterSpec struct {
// +optional
Networks []NetworkSpec `json:"networks,omitempty"`

// Routers define routers for this cluster.
// Each router can be attached to one or more networks via its interfaces.
// If empty, no router is provisioned.
// +listType=map
// +listMapKey=name
// +optional
Routers []RouterSpec `json:"routers,omitempty"`
Comment thread
alakae marked this conversation as resolved.
Resolved

// ControlPlaneLoadBalancer configures the load balancer for the control plane.
// +optional
ControlPlaneLoadBalancer LoadBalancerSpec `json:"controlPlaneLoadBalancer,omitzero"`
Expand Down Expand Up @@ -127,16 +135,68 @@ type NetworkSpec struct {
// +optional
CIDR string `json:"cidr,omitempty"`

// MTU defines the MTU size for this network.
// Immutable after creation
// +kubebuilder:validation:Minimum=1400
// +kubebuilder:validation:Maximum=9000
// +optional
MTU int `json:"mtu,omitempty"`

// GatewayAddress is the gateway IP address for the subnet.
// Only applicable when CIDR is set (managed network).
// By default, no gateway is configured on the subnet. This ensures
// that outbound internet traffic uses the public network interface.
// Set this to a specific IP address (e.g., "10.0.0.1") only if you have configured
// a NAT gateway or similar infrastructure on the private network.
// Only applicable when CIDR is set (managed network). Immutable after creation.
// +optional
GatewayAddress string `json:"gatewayAddress,omitempty"`
}
Comment thread
alakae marked this conversation as resolved.
Resolved

// RouterSpec defines a router managed or adopted for this cluster.
// Set UUID to adopt a pre-existing router; leave it empty to have CAPCS create one.
type RouterSpec struct {
// Name identifies this router within the cluster.
// +kubebuilder:validation:Required
// +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`
// +kubebuilder:validation:MaxLength=63
Name string `json:"name"`

// UUID references a pre-existing router by UUID.
// CAPCS attaches/detaches its interfaces but does not manage the router lifecycle.
// Interfaces already attached to the router must be adopted explicitly via interfaces[].uuid.
// Mutually exclusive with InternetGateway. Immutable after creation.
// +optional
UUID string `json:"uuid,omitempty"`

// InternetGateway enables SNAT for outbound internet access on a managed router.
// Only valid when UUID is not set. Immutable after creation.
// +optional
InternetGateway bool `json:"internetGateway,omitempty"`

// Interfaces lists the networks this router is attached to.
// A router attaches to any given network at most once.
// +listType=map
// +listMapKey=network
// +optional
Interfaces []RouterInterfaceSpec `json:"interfaces,omitempty"`
}

// RouterInterfaceSpec defines a single network attachment for a router.
type RouterInterfaceSpec struct {
// Network references spec.networks[].name.
// +kubebuilder:validation:Required
Network string `json:"network"`

// UUID adopts a pre-existing interface of the router instead of attaching a new one.
// CAPCS reads its address but never creates or detaches it.
// Only valid on an adopted router. Mutually exclusive with Address.
// Immutable after creation.
// +optional
UUID string `json:"uuid,omitempty"`

// Address is the IP requested for this router interface within the referenced network's subnet.
// Must be set explicitly. Must be empty when this interface is adopted by UUID.
// Immutable after creation.
// +optional
Address string `json:"address,omitempty"`
}

// LoadBalancerSpec defines the load balancer configuration for the control plane.
type LoadBalancerSpec struct {
// Enabled controls whether a load balancer is created for the control plane.
Expand Down Expand Up @@ -172,11 +232,18 @@ type LoadBalancerSpec struct {

// Network places the LB VIP on a private network (internal LB).
// References spec.networks[].name. Omit for a public LB.
// When multiple networks are defined this field is required so the LB
// pool members can be registered against a specific subnet.
// When multiple networks are defined, either this field or PoolMemberNetwork
// is required so the LB pool members can be registered against a specific subnet.
// +optional
Network string `json:"network,omitempty"`

// PoolMemberNetwork selects the network whose subnet the LB pool members are registered on (i.e. the network the control-plane machines attach to).
// References spec.networks[].name. Set when control-plane nodes live on a different network than the VIP (Network).
// When unset, the controller resolves the member subnet from Network or the first network.
// Strictly immutable: it cannot be changed, nor set after creation.
// +optional
PoolMemberNetwork string `json:"poolMemberNetwork,omitempty"`

// HealthMonitor configures the load balancer health monitor.
// +optional
HealthMonitor HealthMonitorSpec `json:"healthMonitor,omitempty"`
Expand Down Expand Up @@ -248,6 +315,12 @@ type CloudscaleClusterStatus struct {
// +optional
Networks []NetworkStatus `json:"networks,omitempty"`

// Routers track the provisioned state of each router defined in spec.routers.
// +listType=map
// +listMapKey=name
// +optional
Routers []RouterStatus `json:"routers,omitempty"`

// FloatingIP is the cloudscale.ch floating IP.
// +optional
FloatingIP string `json:"floatingIP,omitempty"`
Expand Down Expand Up @@ -297,9 +370,62 @@ type NetworkStatus struct {
// +optional
CIDR string `json:"cidr,omitempty"`

// MTU is the MTU of this network.
// +optional
MTU int `json:"mtu,omitempty"`

// Managed indicates whether CAPCS manages this network's lifecycle.
// false for pre-existing networks (referenced by UUID), true for CAPCS-created networks (defined by CIDR).
Managed bool `json:"managed"`

// GatewayAddress is the subnet gateway IP that CAPCS has configured on the router interface.
// Empty until the router interface is attached and the subnet gateway is updated.
// +optional
GatewayAddress string `json:"gatewayAddress,omitempty"`
}

// RouterStatus tracks the provisioned state of a single router.
type RouterStatus struct {
// Name matches spec.routers[].name.
Name string `json:"name"`

// RouterID is the router UUID.
// +optional
RouterID string `json:"routerID,omitempty"`

// Interfaces tracks this router's interfaces, at most one per network.
// +listType=map
// +listMapKey=network
// +optional
Interfaces []RouterInterfaceStatus `json:"interfaces,omitempty"`

// Managed indicates whether CAPCS manages this router's lifecycle.
// false for pre-existing routers (referenced by UUID), true for CAPCS-created routers.
Managed bool `json:"managed"`
}

// RouterInterfaceStatus tracks one router interface.
type RouterInterfaceStatus struct {
// Network matches spec.networks[].name.
Network string `json:"network"`

// InterfaceID is the router interface UUID.
// +optional
InterfaceID string `json:"interfaceID,omitempty"`

// Managed indicates whether CAPCS created this interface (spec.uuid was empty).
Managed bool `json:"managed"`
}

// GetInterfaceStatus returns the RouterInterfaceStatus for the given network name, or
// nil if the router has no recorded interface on that network.
func (rs *RouterStatus) GetInterfaceStatus(network string) *RouterInterfaceStatus {
for i := range rs.Interfaces {
if rs.Interfaces[i].Network == network {
return &rs.Interfaces[i]
}
}
return nil
}

// ClusterInitializationStatus contains v1beta2 initialization tracking for CloudscaleCluster.
Expand All @@ -320,6 +446,16 @@ func (s *CloudscaleClusterStatus) GetNetworkStatus(name string) *NetworkStatus {
return nil
}

// GetRouterStatus returns the RouterStatus for the given router name, or nil if not found.
func (s *CloudscaleClusterStatus) GetRouterStatus(name string) *RouterStatus {
for i := range s.Routers {
if s.Routers[i].Name == name {
return &s.Routers[i]
}
}
return nil
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=cloudscaleclusters,scope=Namespaced,categories=cluster-api
Expand Down
19 changes: 19 additions & 0 deletions api/v1beta2/condition_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const (
// True when the pause annotation is present on the resource or parent cluster.
PausedCondition = "Paused"

// RouterReadyCondition indicates all spec.routers are provisioned, active, and have their
// interfaces attached. Also set to True when no routers are defined.
RouterReadyCondition = "RouterReady"

// FloatingIPReadyCondition indicates whether the floating IP has been provisioned and assigned.
// Only applicable to CloudscaleCluster when spec.floatingIP is set.
FloatingIPReadyCondition = "FloatingIPReady"
Expand Down Expand Up @@ -65,6 +69,21 @@ const (
// LoadBalancerDeletingReason indicates the load balancer is being deleted.
LoadBalancerDeletingReason = "LoadBalancerDeleting"

// RouterProvisionedReason indicates all routers and their interfaces are provisioned.
RouterProvisionedReason = "RouterProvisioned"

// RouterNotReadyReason indicates a router exists but its status is not yet active.
RouterNotReadyReason = "RouterNotReady"

// RouterErrorReason indicates an error occurred during router operations.
RouterErrorReason = "RouterError"

// RouterDisabledReason indicates spec.routers is empty.
RouterDisabledReason = "RouterDisabled"

// RouterDeletingReason indicates routers are being deleted.
RouterDeletingReason = "RouterDeleting"

// FloatingIPProvisionedReason indicates the floating IP has been successfully provisioned and assigned.
FloatingIPProvisionedReason = "FloatingIPProvisioned"

Expand Down
Loading
Loading