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
157 changes: 151 additions & 6 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,35 @@ 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.Defaulter so a webhook will be registered for the type.
func (r *HetznerCluster) Default() {}
// 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 {
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)
}
}

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 +149,55 @@ 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 no ID is given check the other network settings for valid entries.
if r.Spec.HCloudNetwork.NetworkZone != nil {
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 {
_, _, 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 {
_, _, 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 +250,46 @@ 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 !reflect.DeepEqual(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 {
// 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 r.Spec.HCloudNetwork.ID != nil {
if errs := areCIDRsAndNetworkZoneEmpty(r.Spec.HCloudNetwork); errs != nil {
allErrs = append(allErrs, errs...)
}
} else {
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 Down Expand Up @@ -225,3 +347,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.
// Defaults 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.
// Defaults 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.

Original file line number Diff line number Diff line change
Expand Up @@ -190,29 +190,33 @@ spec:
for Hetzner Cloud. If left empty, no private Network is configured.
properties:
cidrBlock:
default: 10.0.0.0/16
description: CIDRBlock defines the cidrBlock of the HCloud Network.
If omitted, default "10.0.0.0/16" will be used.
description: |-
CIDRBlock defines the cidrBlock of the HCloud Network.
The webhook defaults this to "10.0.0.0/16".
Mutually exclusive with ID.
type: string
enabled:
description: Enabled defines whether the network should be enabled
or not.
type: boolean
id:
description: |-
ID is the id of the Network to adopt.
Mutually exclusive with CIDRBlock, SubnetCIDRBlock and NetworkZone.
format: int64
type: integer
networkZone:
default: eu-central
description: |-
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.
enum:
- eu-central
- us-east
- us-west
- ap-southeast
The zones must be one of eu-central, us-east, or us-west.
Defaults to "eu-central".
Mutually exclusive with ID.
type: string
subnetCidrBlock:
default: 10.0.0.0/24
description: |-
SubnetCIDRBlock defines the cidrBlock for the subnet of the HCloud Network.
Defaults to "10.0.0.0/24".
Mutually exclusive with ID.
Note: A subnet is required.
type: string
required:
Expand Down Expand Up @@ -466,6 +470,10 @@ spec:
id:
format: int64
type: integer
labels:
additionalProperties:
type: string
type: object
type: object
ready:
default: false
Expand Down
Loading
Loading