diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml index d67cf97022..cdded84c49 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -45,6 +45,12 @@ spec: type: object spec: properties: + additionalTags: + additionalProperties: + type: string + description: AdditionalTags are user-defined tags to be added on the + AWS resources associated with the control plane. + type: object autoscaling: description: Autoscaling specifies auto scaling behaviour for the MachinePools. @@ -100,6 +106,15 @@ spec: type: string type: object x-kubernetes-map-type: atomic + etcdEncryptionKMSArn: + description: EtcdEncryptionKMSArn is the ARN of the KMS key used to + encrypt etcd. The key itself needs to be created out-of-band by + the user and tagged with `red-hat:true`. + maxLength: 2048 + type: string + x-kubernetes-validations: + - message: etcdEncryptionKMSArn must be a valid encryption key ARN + rule: self.matches('^arn:aws[\\w-]*:kms:[\\w-]+:\\d{12}:key\\/(mrk-[0-9a-f]{32}$|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)') identityRef: description: IdentityRef is a reference to an identity to be used when reconciling the managed control plane. If no identity is specified, @@ -162,6 +177,13 @@ spec: oidcID: description: The ID of the OpenID Connect Provider. type: string + private: + description: Private restricts master API endpoint and application + routes to direct, private connectivity. Traffic to these endpoints + will use AWS PrivateLink to have connectivity between VPCs, AWS + services, and your on-premises networks without exposing your traffic + to the public internet. + type: boolean region: description: The AWS Region the cluster lives in. type: string @@ -361,7 +383,7 @@ spec: status: properties: conditions: - description: Conditions specifies the cpnditions for the managed control + description: Conditions specifies the conditions for the managed control plane items: description: Condition defines an observation of a Cluster API resource @@ -428,12 +450,12 @@ spec: description: ID is the cluster ID given by ROSA. type: string initialized: - description: Initialized denotes whether or not the control plane - has the uploaded kubernetes config-map. + description: Initialized denotes whether the control plane has the + uploaded kubernetes config-map. type: boolean oidcEndpointURL: description: OIDCEndpointURL is the endpoint url for the managed OIDC - porvider. + provider. type: string ready: default: false diff --git a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go index 65ecd9279b..3c7d8180ae 100644 --- a/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go +++ b/controlplane/rosa/api/v1beta2/rosacontrolplane_types.go @@ -99,6 +99,26 @@ type RosaControlPlaneSpec struct { //nolint: maligned // Autoscaling specifies auto scaling behaviour for the MachinePools. // +optional Autoscaling *expinfrav1.RosaMachinePoolAutoScaling `json:"autoscaling,omitempty"` + + // +kubebuilder:validation:Optional + + // AdditionalTags are user-defined tags to be added on the AWS resources associated with the control plane. + AdditionalTags infrav1.Tags `json:"additionalTags,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MaxLength=2048 + // +kubebuilder:validation:XValidation:rule=`self.matches('^arn:aws[\\w-]*:kms:[\\w-]+:\\d{12}:key\\/(mrk-[0-9a-f]{32}$|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$)')`, message="etcdEncryptionKMSArn must be a valid encryption key ARN" + + // EtcdEncryptionKMSArn is the ARN of the KMS key used to encrypt etcd. The key itself needs to be + // created out-of-band by the user and tagged with `red-hat:true`. + EtcdEncryptionKMSArn string `json:"etcdEncryptionKMSArn,omitempty"` + + // +kubebuilder:validation:Optional + + // Private restricts master API endpoint and application routes to direct, private connectivity. + // Traffic to these endpoints will use AWS PrivateLink to have connectivity between VPCs, AWS services, + // and your on-premises networks without exposing your traffic to the public internet. + Private bool `json:"private,omitempty"` } // NetworkSpec for ROSA-HCP. @@ -511,7 +531,7 @@ type RosaControlPlaneStatus struct { // is managed by an external service such as AKS, EKS, GKE, etc. // +kubebuilder:default=true ExternalManagedControlPlane *bool `json:"externalManagedControlPlane,omitempty"` - // Initialized denotes whether or not the control plane has the + // Initialized denotes whether the control plane has the // uploaded kubernetes config-map. // +optional Initialized bool `json:"initialized"` @@ -529,17 +549,31 @@ type RosaControlPlaneStatus struct { // // +optional FailureMessage *string `json:"failureMessage,omitempty"` - // Conditions specifies the cpnditions for the managed control plane + // Conditions specifies the conditions for the managed control plane Conditions clusterv1.Conditions `json:"conditions,omitempty"` // ID is the cluster ID given by ROSA. ID *string `json:"id,omitempty"` // ConsoleURL is the url for the openshift console. ConsoleURL string `json:"consoleURL,omitempty"` - // OIDCEndpointURL is the endpoint url for the managed OIDC porvider. + // OIDCEndpointURL is the endpoint url for the managed OIDC provider. OIDCEndpointURL string `json:"oidcEndpointURL,omitempty"` } +const ( + // OCMCredentialsValidCondition reports whether the OCM credentials are valid. + OCMCredentialsValidCondition clusterv1.ConditionType = "OCMCredentialsValid" + // ROSAControlPlaneConfiguredCondition reports whether the ROSA control plane is configured correctly. + ROSAControlPlaneConfiguredCondition clusterv1.ConditionType = "ROSAControlPlaneConfigured" +) + +const ( + // OCMCredentialsImproperReason denotes that the OCM credentials are not proper. + OCMCredentialsImproperReason = "OCMCredentialsImproper" + // ConfigurationMalformedReason denotes that the configuration provided by the user is malformed. + ConfigurationMalformedReason = "ConfigurationMalformed" +) + // +kubebuilder:object:root=true // +kubebuilder:resource:path=rosacontrolplanes,shortName=rosacp,scope=Namespaced,categories=cluster-api // +kubebuilder:storageversion diff --git a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go index 41dac04354..d1403b9728 100644 --- a/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/rosa/api/v1beta2/zz_generated.deepcopy.go @@ -177,6 +177,13 @@ func (in *RosaControlPlaneSpec) DeepCopyInto(out *RosaControlPlaneSpec) { *out = new(expapiv1beta2.RosaMachinePoolAutoScaling) **out = **in } + if in.AdditionalTags != nil { + in, out := &in.AdditionalTags, &out.AdditionalTags + *out = make(apiv1beta2.Tags, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RosaControlPlaneSpec. diff --git a/controlplane/rosa/controllers/rosacontrolplane_controller.go b/controlplane/rosa/controllers/rosacontrolplane_controller.go index 69e6e31c8a..7adfdf6e2f 100644 --- a/controlplane/rosa/controllers/rosacontrolplane_controller.go +++ b/controlplane/rosa/controllers/rosacontrolplane_controller.go @@ -177,11 +177,6 @@ func (r *ROSAControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Req func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaScope *scope.ROSAControlPlaneScope) (res ctrl.Result, reterr error) { rosaScope.Info("Reconciling ROSAControlPlane") - - // if !rosaScope.Cluster.Status.InfrastructureReady { - // rosaScope.Info("Cluster infrastructure is not ready yet") - // return ctrl.Result{RequeueAfter: r.WaitInfraPeriod}, nil - //} if controllerutil.AddFinalizer(rosaScope.ControlPlane, ROSAControlPlaneFinalizer) { if err := rosaScope.PatchObject(); err != nil { return ctrl.Result{}, err @@ -190,8 +185,8 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc ocmClient, err := rosa.NewOCMClient(ctx, rosaScope) if err != nil { - // TODO: need to expose in status, as likely the credentials are invalid - return ctrl.Result{}, fmt.Errorf("failed to create OCM client: %w", err) + conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.OCMCredentialsValidCondition, rosacontrolplanev1.OCMCredentialsImproperReason, clusterv1.ConditionSeverityError, err.Error()) + return ctrl.Result{}, nil } creator, err := rosaaws.CreatorForCallerIdentity(rosaScope.Identity) @@ -202,11 +197,36 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc if validationMessage, validationError := validateControlPlaneSpec(ocmClient, rosaScope); validationError != nil { return ctrl.Result{}, fmt.Errorf("validate ROSAControlPlane.spec: %w", validationError) } else if validationMessage != "" { - rosaScope.ControlPlane.Status.FailureMessage = ptr.To(validationMessage) - // dont' requeue because input is invalid and manual intervention is needed. + conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneConfiguredCondition, rosacontrolplanev1.ConfigurationMalformedReason, clusterv1.ConditionSeverityError, validationMessage) return ctrl.Result{}, nil + } + + var machineCIDR, podCIDR, serviceCIDR *net.IPNet + if _, cidr, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.MachineCIDR); err == nil { + machineCIDR = cidr } else { - rosaScope.ControlPlane.Status.FailureMessage = nil + conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneConfiguredCondition, rosacontrolplanev1.ConfigurationMalformedReason, clusterv1.ConditionSeverityError, fmt.Sprintf("rosacontrolplane.spec.network.machineCIDR invalid: %v", err)) + return ctrl.Result{}, nil + } + + if rosaScope.ControlPlane.Spec.Network.PodCIDR != "" { + _, cidr, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.PodCIDR) + if err == nil { + podCIDR = cidr + } else { + conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneConfiguredCondition, rosacontrolplanev1.ConfigurationMalformedReason, clusterv1.ConditionSeverityError, fmt.Sprintf("rosacontrolplane.spec.network.podCIDR invalid: %v", err)) + return ctrl.Result{}, nil + } + } + + if rosaScope.ControlPlane.Spec.Network.ServiceCIDR != "" { + _, cidr, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.ServiceCIDR) + if err == nil { + serviceCIDR = cidr + } else { + conditions.MarkFalse(rosaScope.ControlPlane, rosacontrolplanev1.ROSAControlPlaneConfiguredCondition, rosacontrolplanev1.ConfigurationMalformedReason, clusterv1.ConditionSeverityError, fmt.Sprintf("rosacontrolplane.spec.network.serviceCIDR invalid: %v", err)) + return ctrl.Result{}, nil + } } cluster, err := ocmClient.GetCluster(rosaScope.ControlPlane.Spec.RosaClusterName, creator) @@ -278,6 +298,11 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc DisableWorkloadMonitoring: ptr.To(true), DefaultIngress: ocm.NewDefaultIngressSpec(), // n.b. this is a no-op when it's set to the default value ComputeMachineType: rosaScope.ControlPlane.Spec.InstanceType, + Tags: rosaScope.ControlPlane.Spec.AdditionalTags, + EtcdEncryption: rosaScope.ControlPlane.Spec.EtcdEncryptionKMSArn != "", + EtcdEncryptionKMSArn: rosaScope.ControlPlane.Spec.EtcdEncryptionKMSArn, + Private: &rosaScope.ControlPlane.Spec.Private, + PrivateLink: &rosaScope.ControlPlane.Spec.Private, // all private ROSA HCP clusters are privateLink SubnetIds: rosaScope.ControlPlane.Spec.Subnets, AvailabilityZones: rosaScope.ControlPlane.Spec.AvailabilityZones, @@ -295,36 +320,14 @@ func (r *ROSAControlPlaneReconciler) reconcileNormal(ctx context.Context, rosaSc BillingAccount: billingAccount, AWSCreator: creator, } - - _, machineCIDR, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.MachineCIDR) - if err == nil { + if machineCIDR != nil { spec.MachineCIDR = *machineCIDR - } else { - // TODO: expose in status - rosaScope.Error(err, "rosacontrolplane.spec.network.machineCIDR invalid", rosaScope.ControlPlane.Spec.Network.MachineCIDR) - return ctrl.Result{}, nil } - - if rosaScope.ControlPlane.Spec.Network.PodCIDR != "" { - _, podCIDR, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.PodCIDR) - if err == nil { - spec.PodCIDR = *podCIDR - } else { - // TODO: expose in status. - rosaScope.Error(err, "rosacontrolplane.spec.network.podCIDR invalid", rosaScope.ControlPlane.Spec.Network.PodCIDR) - return ctrl.Result{}, nil - } + if podCIDR != nil { + spec.PodCIDR = *podCIDR } - - if rosaScope.ControlPlane.Spec.Network.ServiceCIDR != "" { - _, serviceCIDR, err := net.ParseCIDR(rosaScope.ControlPlane.Spec.Network.ServiceCIDR) - if err == nil { - spec.ServiceCIDR = *serviceCIDR - } else { - // TODO: expose in status. - rosaScope.Error(err, "rosacontrolplane.spec.network.serviceCIDR invalid", rosaScope.ControlPlane.Spec.Network.ServiceCIDR) - return ctrl.Result{}, nil - } + if serviceCIDR != nil { + spec.ServiceCIDR = *serviceCIDR } // Set autoscale replica @@ -588,7 +591,13 @@ func validateControlPlaneSpec(ocmClient *ocm.Client, rosaScope *scope.ROSAContro return fmt.Sprintf("version %s is not supported", version), nil } - // TODO: add more input validations + if errs := rosaScope.ControlPlane.Spec.AdditionalTags.Validate(); errs != nil { + var msg []string + for _, err := range errs { + msg = append(msg, err.Error()) + } + return strings.Join(msg, ", "), nil + } return "", nil }