Skip to content

✨ Bring your own network #1472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions api/v1beta1/conditions_const.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const (
NetworkReadyCondition clusterv1.ConditionType = "NetworkReady"
// NetworkReconcileFailedReason indicates that reconciling the network failed.
NetworkReconcileFailedReason = "NetworkReconcileFailed"
// MultipleSubnetsExistReason indicates that the network has multiple subnets.
MultipleSubnetsExistReason = "MultipleSubnetsExist"
)

const (
Expand Down
187 changes: 179 additions & 8 deletions api/v1beta1/hetznercluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ limitations under the License.
package v1beta1

import (
"context"
"fmt"
"net"
"reflect"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
Expand All @@ -42,10 +45,22 @@ var regionNetworkZoneMap = map[string]string{
"sin": "ap-southeast",
}

const (
// DefaultCIDRBlock specifies the default CIDR block used by the HCloudNetwork.
DefaultCIDRBlock = "10.0.0.0/16"

// DefaultSubnetCIDRBlock specifies the default subnet CIDR block used by the HCloudNetwork.
DefaultSubnetCIDRBlock = "10.0.0.0/24"

// DefaultNetworkZone specifies the default network zone used by the HCloudNetwork.
DefaultNetworkZone = "eu-central"
)

// SetupWebhookWithManager initializes webhook manager for HetznerCluster.
func (r *HetznerCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
WithDefaulter(r).
Complete()
}

Expand All @@ -60,10 +75,37 @@ func (r *HetznerClusterList) SetupWebhookWithManager(mgr ctrl.Manager) error {

//+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-hetznercluster,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hetznerclusters,verbs=create;update,versions=v1beta1,name=mutation.hetznercluster.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}

var _ webhook.Defaulter = &HetznerCluster{}
var _ webhook.CustomDefaulter = &HetznerCluster{}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the type.
func (r *HetznerCluster) Default(_ context.Context, obj runtime.Object) error {
hetznerclusterlog.V(1).Info("default", "name", r.Name)

cluster, ok := obj.(*HetznerCluster)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected an HetznerCluster but got a %T", obj))
}

if !cluster.Spec.HCloudNetwork.Enabled {
return nil
}

if cluster.Spec.HCloudNetwork.ID != nil {
return nil
}

if cluster.Spec.HCloudNetwork.CIDRBlock == nil {
cluster.Spec.HCloudNetwork.CIDRBlock = ptr.To(DefaultCIDRBlock)
}
if cluster.Spec.HCloudNetwork.SubnetCIDRBlock == nil {
cluster.Spec.HCloudNetwork.SubnetCIDRBlock = ptr.To(DefaultSubnetCIDRBlock)
}
if cluster.Spec.HCloudNetwork.NetworkZone == nil {
cluster.Spec.HCloudNetwork.NetworkZone = ptr.To[HCloudNetworkZone](DefaultNetworkZone)
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (r *HetznerCluster) Default() {}
return nil
}

//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-hetznercluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=hetznerclusters,verbs=create;update,versions=v1beta1,name=validation.hetznercluster.infrastructure.cluster.x-k8s.io,admissionReviewVersions={v1,v1beta1}

Expand Down Expand Up @@ -109,6 +151,73 @@ func (r *HetznerCluster) ValidateCreate() (admission.Warnings, error) {
if err := isNetworkZoneSameForAllRegions(r.Spec.ControlPlaneRegions, nil); err != nil {
allErrs = append(allErrs, err)
}
} else {
// If ID is given check that all other network settings are empty.
if r.Spec.HCloudNetwork.ID != nil {
if errs := areCIDRsAndNetworkZoneEmpty(r.Spec.HCloudNetwork); errs != nil {
allErrs = append(allErrs, errs...)
}
} else {
if r.Spec.HCloudNetwork.NetworkZone == nil {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "networkZone"),
r.Spec.HCloudNetwork.NetworkZone,
"network zone must not be nil when hcloudNetwork is enabled"),
)
// If no ID is given check the other network settings for valid entries.
} else {
givenZone := string(*r.Spec.HCloudNetwork.NetworkZone)

var validNetworkZone bool
for _, z := range regionNetworkZoneMap {
if givenZone == z {
validNetworkZone = true
break
}
}
if !validNetworkZone {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "networkZone"),
r.Spec.HCloudNetwork.NetworkZone,
"wrong network zone. Should be eu-central, us-east, us-west or ap-southeast"),
)
}
}

if r.Spec.HCloudNetwork.CIDRBlock == nil {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "cidrBlock"),
r.Spec.HCloudNetwork.NetworkZone,
"cidrBlock must not be nil when hcloudNetwork is enabled"),
)
} else {
_, _, err := net.ParseCIDR(*r.Spec.HCloudNetwork.CIDRBlock)
if err != nil {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "cidrBlock"),
r.Spec.HCloudNetwork.CIDRBlock,
"malformed cidrBlock"),
)
}
}

if r.Spec.HCloudNetwork.SubnetCIDRBlock == nil {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "subnetCIDRBlock"),
r.Spec.HCloudNetwork.SubnetCIDRBlock,
"subnetCIDRBlock must not be nil when hcloudNetwork is enabled"),
)
} else {
_, _, err := net.ParseCIDR(*r.Spec.HCloudNetwork.SubnetCIDRBlock)
if err != nil {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "hcloudNetwork", "subnetCIDRBlock"),
r.Spec.HCloudNetwork.SubnetCIDRBlock,
"malformed cidrBlock"),
)
}
}
}
}

// Check whether controlPlaneEndpoint is specified if allow empty is not set or false
Expand Down Expand Up @@ -161,13 +270,52 @@ func (r *HetznerCluster) ValidateUpdate(old runtime.Object) (admission.Warnings,
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an HetznerCluster but got a %T", old))
}

// Network settings are immutable
if !reflect.DeepEqual(oldC.Spec.HCloudNetwork, r.Spec.HCloudNetwork) {
if oldC.Spec.HCloudNetwork.Enabled != r.Spec.HCloudNetwork.Enabled {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork"), r.Spec.HCloudNetwork, "field is immutable"),
field.Invalid(field.NewPath("spec", "hcloudNetwork", "enabled"), r.Spec.HCloudNetwork.Enabled, "field is immutable"),
)
}

if !oldC.Spec.HCloudNetwork.Enabled {
// If the network is disabled check that all other network related fields are empty.
if r.Spec.HCloudNetwork.ID != nil {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "id"), oldC.Spec.HCloudNetwork.ID, "field must be empty"),
)
}
if errs := areCIDRsAndNetworkZoneEmpty(r.Spec.HCloudNetwork); errs != nil {
allErrs = append(allErrs, errs...)
}
}

if oldC.Spec.HCloudNetwork.Enabled {
// Only allow updating the network ID when it was not set previously. This makes it possible to e.g. adopt the
// network that was created initially by CAPH.
if oldC.Spec.HCloudNetwork.ID != nil && !reflect.DeepEqual(oldC.Spec.HCloudNetwork.ID, r.Spec.HCloudNetwork.ID) {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "id"), r.Spec.HCloudNetwork.ID, "field is immutable"),
)
}

if !reflect.DeepEqual(oldC.Spec.HCloudNetwork.CIDRBlock, r.Spec.HCloudNetwork.CIDRBlock) {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "cidrBlock"), r.Spec.HCloudNetwork.CIDRBlock, "field is immutable"),
)
}

if !reflect.DeepEqual(oldC.Spec.HCloudNetwork.SubnetCIDRBlock, r.Spec.HCloudNetwork.SubnetCIDRBlock) {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "subnetCIDRBlock"), r.Spec.HCloudNetwork.SubnetCIDRBlock, "field is immutable"),
)
}

if !reflect.DeepEqual(oldC.Spec.HCloudNetwork.NetworkZone, r.Spec.HCloudNetwork.NetworkZone) {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "networkZone"), r.Spec.HCloudNetwork.NetworkZone, "field is immutable"),
)
}
}

// Check if all regions are in the same network zone if a private network is enabled
if oldC.Spec.HCloudNetwork.Enabled {
var defaultNetworkZone *string
Expand All @@ -182,14 +330,14 @@ func (r *HetznerCluster) ValidateUpdate(old runtime.Object) (admission.Warnings,
}

// Load balancer enabled/disabled is immutable
if !reflect.DeepEqual(oldC.Spec.ControlPlaneLoadBalancer.Enabled, r.Spec.ControlPlaneLoadBalancer.Enabled) {
if oldC.Spec.ControlPlaneLoadBalancer.Enabled != r.Spec.ControlPlaneLoadBalancer.Enabled {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "enabled"), r.Spec.ControlPlaneLoadBalancer.Enabled, "field is immutable"),
)
}

// Load balancer region and port are immutable
if !reflect.DeepEqual(oldC.Spec.ControlPlaneLoadBalancer.Port, r.Spec.ControlPlaneLoadBalancer.Port) {
if oldC.Spec.ControlPlaneLoadBalancer.Port != r.Spec.ControlPlaneLoadBalancer.Port {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "controlPlaneLoadBalancer", "port"), r.Spec.ControlPlaneLoadBalancer.Port, "field is immutable"),
)
Expand Down Expand Up @@ -225,3 +373,26 @@ func (r *HetznerCluster) ValidateDelete() (admission.Warnings, error) {
hetznerclusterlog.V(1).Info("validate delete", "name", r.Name)
return nil, nil
}

func areCIDRsAndNetworkZoneEmpty(hcloudNetwork HCloudNetworkSpec) field.ErrorList {
var allErrs field.ErrorList
if hcloudNetwork.CIDRBlock != nil {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "cidrBlock"), hcloudNetwork.CIDRBlock, "field must be empty"),
)
}

if hcloudNetwork.SubnetCIDRBlock != nil {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "subnetCIDRBlock"), hcloudNetwork.SubnetCIDRBlock, "field must be empty"),
)
}

if hcloudNetwork.NetworkZone != nil {
allErrs = append(allErrs,
field.Invalid(field.NewPath("spec", "hcloudNetwork", "networkZone"), hcloudNetwork.NetworkZone, "field must be empty"),
)
}

return allErrs
}
31 changes: 19 additions & 12 deletions api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,32 +217,39 @@ type LoadBalancerTarget struct {

// HCloudNetworkSpec defines the desired state of the HCloud Private Network.
type HCloudNetworkSpec struct {
// ID is the id of the Network to adopt.
// Mutually exclusive with CIDRBlock, SubnetCIDRBlock and NetworkZone.
// +optional
ID *int64 `json:"id,omitempty"`

// Enabled defines whether the network should be enabled or not.
Enabled bool `json:"enabled"`

// CIDRBlock defines the cidrBlock of the HCloud Network. If omitted, default "10.0.0.0/16" will be used.
// +kubebuilder:default="10.0.0.0/16"
// CIDRBlock defines the cidrBlock of the HCloud Network.
// The webhook defaults this to "10.0.0.0/16".
// Mutually exclusive with ID.
// +optional
CIDRBlock string `json:"cidrBlock,omitempty"`
CIDRBlock *string `json:"cidrBlock,omitempty"`

// SubnetCIDRBlock defines the cidrBlock for the subnet of the HCloud Network.
// The webhook defaults this to "10.0.0.0/24".
// Mutually exclusive with ID.
// Note: A subnet is required.
// +kubebuilder:default="10.0.0.0/24"
// +optional
SubnetCIDRBlock string `json:"subnetCidrBlock,omitempty"`
SubnetCIDRBlock *string `json:"subnetCidrBlock,omitempty"`

// NetworkZone specifies the HCloud network zone of the private network.
// The zones must be one of eu-central, us-east, or us-west. The default is eu-central.
// +kubebuilder:validation:Enum=eu-central;us-east;us-west;ap-southeast
// +kubebuilder:default=eu-central
// The zones must be one of eu-central, us-east, or us-west.
// The webhook defaults this to "eu-central".
// Mutually exclusive with ID.
// +optional
NetworkZone HCloudNetworkZone `json:"networkZone,omitempty"`
NetworkZone *HCloudNetworkZone `json:"networkZone,omitempty"`
}

// NetworkStatus defines the observed state of the HCloud Private Network.
type NetworkStatus struct {
ID int64 `json:"id,omitempty"`
Labels map[string]string `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
AttachedServers []int64 `json:"attachedServers,omitempty"`
}

Expand All @@ -255,10 +262,10 @@ type HCloudNetworkZone string

// IsZero returns true if a private Network is set.
func (s *HCloudNetworkSpec) IsZero() bool {
if s.CIDRBlock != "" {
if s.CIDRBlock != nil {
return false
}
if s.SubnetCIDRBlock != "" {
if s.SubnetCIDRBlock != nil {
return false
}
return true
Expand Down
22 changes: 21 additions & 1 deletion api/v1beta1/zz_generated.deepcopy.go

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

Loading
Loading