diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosanetworks.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosanetworks.yaml index a94b3e8d94..ee967d5749 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosanetworks.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosanetworks.yaml @@ -44,19 +44,26 @@ spec: description: ROSANetworkSpec defines the desired state of ROSANetwork properties: availabilityZoneCount: - default: 1 description: |- The number of availability zones to be used for creation of the network infrastructure. You can specify anything between one and four, depending on the chosen AWS region. + Either AvailabilityZoneCount OR AvailabilityZones must be set. + minimum: 1 type: integer + x-kubernetes-validations: + - message: availabilityZoneCount is immutable + rule: self == oldSelf availabilityZones: description: |- The list of availability zones to be used for creation of the network infrastructure. You can specify anything between one and four valid availability zones from a given region. - Should you specify both the availabilityZoneCount and availabilityZones, the list of availability zones takes preference. + Either AvailabilityZones OR AvailabilityZoneCount must be set. items: type: string type: array + x-kubernetes-validations: + - message: availabilityZones is immutable + rule: self == oldSelf cidrBlock: description: CIDR block to be used for the VPC format: cidr @@ -85,10 +92,16 @@ spec: description: The AWS region in which the components of ROSA network infrastruture are to be crated type: string + x-kubernetes-validations: + - message: region is immutable + rule: self == oldSelf stackName: description: The name of the cloudformation stack under which the network infrastructure would be created type: string + x-kubernetes-validations: + - message: stackName is immutable + rule: self == oldSelf stackTags: additionalProperties: type: string diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0338fde577..9fc33ff9ea 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -214,6 +214,7 @@ rules: - infrastructure.cluster.x-k8s.io resources: - rosamachinepools/finalizers + - rosanetworks/finalizers - rosaroleconfigs/finalizers verbs: - update diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index d48065c09f..38ec935e11 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -223,6 +223,28 @@ webhooks: resources: - rosamachinepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1beta2-rosanetwork + failurePolicy: Fail + matchPolicy: Equivalent + name: default.rosanetwork.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - rosanetworks + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -603,6 +625,28 @@ webhooks: resources: - rosamachinepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1beta2-rosanetwork + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.rosanetwork.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - rosanetworks + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/exp/api/v1beta2/rosanetwork_types.go b/exp/api/v1beta2/rosanetwork_types.go index b6a6d3634f..f16ac85dfb 100644 --- a/exp/api/v1beta2/rosanetwork_types.go +++ b/exp/api/v1beta2/rosanetwork_types.go @@ -29,41 +29,42 @@ const ROSANetworkFinalizer = "rosanetwork.infrastructure.cluster.x-k8s.io" // ROSANetworkSpec defines the desired state of ROSANetwork type ROSANetworkSpec struct { // The name of the cloudformation stack under which the network infrastructure would be created - // +immutable + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="stackName is immutable" + // +kubebuilder:validation:Required StackName string `json:"stackName"` // The AWS region in which the components of ROSA network infrastruture are to be crated - // +immutable + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="region is immutable" + // +kubebuilder:validation:Required Region string `json:"region"` // The number of availability zones to be used for creation of the network infrastructure. // You can specify anything between one and four, depending on the chosen AWS region. - // +kubebuilder:default=1 + // Either AvailabilityZoneCount OR AvailabilityZones must be set. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="availabilityZoneCount is immutable" // +optional - // +immutable - AvailabilityZoneCount int `json:"availabilityZoneCount"` + AvailabilityZoneCount int `json:"availabilityZoneCount,omitempty"` // The list of availability zones to be used for creation of the network infrastructure. // You can specify anything between one and four valid availability zones from a given region. - // Should you specify both the availabilityZoneCount and availabilityZones, the list of availability zones takes preference. + // Either AvailabilityZones OR AvailabilityZoneCount must be set. + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="availabilityZones is immutable" // +optional - // +immutable - AvailabilityZones []string `json:"availabilityZones"` + AvailabilityZones []string `json:"availabilityZones,omitempty"` // CIDR block to be used for the VPC // +kubebuilder:validation:Format=cidr - // +immutable + // +kubebuilder:validation:Required CIDRBlock string `json:"cidrBlock"` // IdentityRef is a reference to an identity to be used when reconciling rosa network. // If no identity is specified, the default identity for this controller will be used. - // // +optional IdentityRef *infrav1.AWSIdentityReference `json:"identityRef,omitempty"` // StackTags is an optional set of tags to add to the created cloudformation stack. // The stack tags will then be automatically applied to the supported AWS resources (VPC, subnets, ...). - // // +optional StackTags Tags `json:"stackTags,omitempty"` } diff --git a/exp/api/v1beta2/rosanetwork_webhook.go b/exp/api/v1beta2/rosanetwork_webhook.go new file mode 100644 index 0000000000..0465d892a2 --- /dev/null +++ b/exp/api/v1beta2/rosanetwork_webhook.go @@ -0,0 +1,73 @@ +package v1beta2 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the webhooks for the ROSANetwork. +func (r *ROSANetwork) SetupWebhookWithManager(mgr ctrl.Manager) error { + w := new(rosaNetworkWebhook) + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(w). + WithDefaulter(w). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2-rosanetwork,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=rosanetworks,versions=v1beta2,name=validation.rosanetwork.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta2-rosanetwork,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=rosanetworks,versions=v1beta2,name=default.rosanetwork.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +type rosaNetworkWebhook struct{} + +var _ webhook.CustomDefaulter = &rosaNetworkWebhook{} +var _ webhook.CustomValidator = &rosaNetworkWebhook{} + +// ValidateCreate implements admission.Validator. +func (r *rosaNetworkWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) { + rosaNet, ok := obj.(*ROSANetwork) + if !ok { + return nil, fmt.Errorf("expected an ROSANetwork object but got %T", rosaNet) + } + + var allErrs field.ErrorList + if rosaNet.Spec.AvailabilityZoneCount == 0 && len(rosaNet.Spec.AvailabilityZones) == 0 { + err := field.Invalid(field.NewPath("spec.AvailabilityZones"), rosaNet.Spec.AvailabilityZones, "Either AvailabilityZones OR AvailabilityZoneCount must be set.") + allErrs = append(allErrs, err) + } + if rosaNet.Spec.AvailabilityZoneCount != 0 && len(rosaNet.Spec.AvailabilityZones) > 0 { + err := field.Invalid(field.NewPath("spec.AvailabilityZones"), rosaNet.Spec.AvailabilityZones, "Either AvailabilityZones OR AvailabilityZoneCount can be set.") + allErrs = append(allErrs, err) + } + + if len(allErrs) > 0 { + return nil, apierrors.NewInvalid( + rosaNet.GroupVersionKind().GroupKind(), + rosaNet.Name, + allErrs) + } + + return nil, nil +} + +// ValidateUpdate implements admission.Validator. +func (r *rosaNetworkWebhook) ValidateUpdate(ctx context.Context, old runtime.Object, updated runtime.Object) (warnings admission.Warnings, err error) { + return nil, nil +} + +// ValidateDelete implements admission.Validator. +func (r *rosaNetworkWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) { + return nil, nil +} + +// Default implements admission.Defaulter. +func (r *rosaNetworkWebhook) Default(ctx context.Context, obj runtime.Object) error { + return nil +} diff --git a/exp/controllers/rosanetwork_controller.go b/exp/controllers/rosanetwork_controller.go index bcc1183cc2..a4bc764e76 100644 --- a/exp/controllers/rosanetwork_controller.go +++ b/exp/controllers/rosanetwork_controller.go @@ -60,6 +60,7 @@ type ROSANetworkReconciler struct { // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosanetworks,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosanetworks/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=rosanetworks/finalizers,verbs=update func (r *ROSANetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) { log := logger.FromContext(ctx) @@ -136,15 +137,20 @@ func (r *ROSANetworkReconciler) reconcileNormal(ctx context.Context, rosaNetScop if r.cfStack == nil { // The CF stack does not exist yet templateBody := string(rosaCFNetwork.CloudFormationTemplateFile) + + zoneCount := 1 + if rosaNetScope.ROSANetwork.Spec.AvailabilityZoneCount > 0 { + zoneCount = rosaNetScope.ROSANetwork.Spec.AvailabilityZoneCount + } cfParams := map[string]string{ - "AvailabilityZoneCount": strconv.Itoa(rosaNetScope.ROSANetwork.Spec.AvailabilityZoneCount), + "AvailabilityZoneCount": strconv.Itoa(zoneCount), "Region": rosaNetScope.ROSANetwork.Spec.Region, "Name": rosaNetScope.ROSANetwork.Spec.StackName, "VpcCidr": rosaNetScope.ROSANetwork.Spec.CIDRBlock, } // Explicitly specified AZs - for i, zone := range rosaNetScope.ROSANetwork.Spec.AvailabilityZones { - cfParams[fmt.Sprintf("AZ%d", i)] = zone + for idx, zone := range rosaNetScope.ROSANetwork.Spec.AvailabilityZones { + cfParams[fmt.Sprintf("AZ%d", (idx+1))] = zone } // Call the AWS CF stack create API diff --git a/exp/controllers/suite_test.go b/exp/controllers/suite_test.go index 637cb6a19e..4daa9b26d9 100644 --- a/exp/controllers/suite_test.go +++ b/exp/controllers/suite_test.go @@ -89,6 +89,9 @@ func setup() { if err := (&expinfrav1.ROSARoleConfig{}).SetupWebhookWithManager(testEnv); err != nil { panic(fmt.Sprintf("Unable to setup ROSARoleConfig webhook: %v", err)) } + if err := (&expinfrav1.ROSANetwork{}).SetupWebhookWithManager(testEnv); err != nil { + panic(fmt.Sprintf("Unable to setup ROSANetwork webhook: %v", err)) + } if err := (&rosacontrolplanev1.ROSAControlPlane{}).SetupWebhookWithManager(testEnv); err != nil { panic(fmt.Sprintf("Unable to setup ROSAControlPlane webhook: %v", err)) } diff --git a/main.go b/main.go index a0fcb0563b..785d1e7969 100644 --- a/main.go +++ b/main.go @@ -291,6 +291,11 @@ func main() { os.Exit(1) } + if err := (&expinfrav1.ROSANetwork{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ROSANetwork") + os.Exit(1) + } + setupLog.Debug("enabling ROSA role config controller") if err = (&expcontrollers.ROSARoleConfigReconciler{ Client: mgr.GetClient(),