diff --git a/api/v1alpha2/azurecluster_conversion.go b/api/v1alpha2/azurecluster_conversion.go index f3c583240e1..cf98e78c6e2 100644 --- a/api/v1alpha2/azurecluster_conversion.go +++ b/api/v1alpha2/azurecluster_conversion.go @@ -57,6 +57,7 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint } dst.Status.FailureDomains = restored.Status.FailureDomains + dst.Spec.NetworkSpec.PublicIP = restored.Spec.NetworkSpec.PublicIP for _, restoredSubnet := range restored.Spec.NetworkSpec.Subnets { if restoredSubnet != nil { diff --git a/api/v1alpha2/zz_generated.conversion.go b/api/v1alpha2/zz_generated.conversion.go index 3691f87db88..7c3b736c50b 100644 --- a/api/v1alpha2/zz_generated.conversion.go +++ b/api/v1alpha2/zz_generated.conversion.go @@ -948,6 +948,7 @@ func autoConvert_v1alpha2_NetworkSpec_To_v1alpha3_NetworkSpec(in *NetworkSpec, o } func autoConvert_v1alpha3_NetworkSpec_To_v1alpha2_NetworkSpec(in *v1alpha3.NetworkSpec, out *NetworkSpec, s conversion.Scope) error { + // WARNING: in.PublicIP requires manual conversion: does not exist in peer-type if err := Convert_v1alpha3_VnetSpec_To_v1alpha2_VnetSpec(&in.Vnet, &out.Vnet, s); err != nil { return err } @@ -1009,9 +1010,9 @@ func Convert_v1alpha2_PublicIP_To_v1alpha3_PublicIP(in *PublicIP, out *v1alpha3. func autoConvert_v1alpha3_PublicIP_To_v1alpha2_PublicIP(in *v1alpha3.PublicIP, out *PublicIP, s conversion.Scope) error { out.ID = in.ID - out.Name = in.Name out.IPAddress = in.IPAddress out.DNSName = in.DNSName + out.Name = in.Name return nil } diff --git a/api/v1alpha3/azurecluster_validation.go b/api/v1alpha3/azurecluster_validation.go index f63b2dae3cd..29fe8169d2c 100644 --- a/api/v1alpha3/azurecluster_validation.go +++ b/api/v1alpha3/azurecluster_validation.go @@ -20,8 +20,6 @@ import ( "fmt" "regexp" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -34,16 +32,8 @@ const ( ) // validateCluster validates a cluster -func (c *AzureCluster) validateCluster() error { - var allErrs field.ErrorList - allErrs = append(allErrs, c.validateClusterSpec()...) - if len(allErrs) == 0 { - return nil - } - - return apierrors.NewInvalid( - schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "AzureCluster"}, - c.Name, allErrs) +func (c *AzureCluster) validateCluster() field.ErrorList { + return c.validateClusterSpec() } // validateClusterSpec validates a ClusterSpec @@ -139,3 +129,16 @@ func validateInternalLBIPAddress(address string, fldPath *field.Path) *field.Err } return nil } + +func validateControlPlaneIP(old, new *PublicIPSpec, fldPath *field.Path) *field.Error { + if old == nil && new != nil { + return field.Invalid(fldPath, new, fmt.Sprintf("setting control plane endpoint after cluster creation is not allowed")) + } + if old != nil && new == nil { + return field.Invalid(fldPath, new, fmt.Sprintf("removing control plane endpoint after cluster creation is not allowed")) + } + if old != nil && new != nil && old.Name != new.Name { + return field.Invalid(fldPath, new, fmt.Sprintf("changing control plane endpoint after cluster creation is not allowed")) + } + return nil +} diff --git a/api/v1alpha3/azurecluster_webhook.go b/api/v1alpha3/azurecluster_webhook.go index e1b30359ae3..23104d88fdb 100644 --- a/api/v1alpha3/azurecluster_webhook.go +++ b/api/v1alpha3/azurecluster_webhook.go @@ -17,7 +17,9 @@ limitations under the License. package v1alpha3 import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -48,15 +50,41 @@ func (c *AzureCluster) Default() { // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (c *AzureCluster) ValidateCreate() error { clusterlog.Info("validate create", "name", c.Name) + allErrs := c.validateCluster() - return c.validateCluster() + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(GroupVersion.WithKind("AzureCluster").GroupKind(), c.Name, allErrs) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (c *AzureCluster) ValidateUpdate(old runtime.Object) error { +func (c *AzureCluster) ValidateUpdate(oldRaw runtime.Object) error { clusterlog.Info("validate update", "name", c.Name) - return c.validateCluster() + old := oldRaw.(*AzureCluster) + + var allErrs field.ErrorList + + // validate cluster may return a list of errors + if errs := c.validateCluster(); errs != nil { + allErrs = append(allErrs, errs...) + } + + if err := validateControlPlaneIP( + old.Spec.NetworkSpec.PublicIP, + c.Spec.NetworkSpec.PublicIP, + field.NewPath("spec").Child("networkSpec").Child("publicIp"), + ); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(GroupVersion.WithKind("AzureCluster").GroupKind(), c.Name, allErrs) } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/api/v1alpha3/types.go b/api/v1alpha3/types.go index 9838ca0b2df..cdfa731913a 100644 --- a/api/v1alpha3/types.go +++ b/api/v1alpha3/types.go @@ -38,6 +38,11 @@ type Network struct { // NetworkSpec specifies what the Azure networking resources should look like. type NetworkSpec struct { + // PublicIP is the public IP to attach to the Azure load balanacer. + // If name is not set in the spec, one will be generated. + // +optional + PublicIP *PublicIPSpec `json:"publicIp,omitempty"` + // Vnet is the configuration for the Azure virtual network. // +optional Vnet VnetSpec `json:"vnet,omitempty"` @@ -137,9 +142,14 @@ type IngressRules []*IngressRule // PublicIP defines an Azure public IP address. type PublicIP struct { ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` IPAddress string `json:"ipAddress,omitempty"` DNSName string `json:"dnsName,omitempty"` + Name string `json:"name"` +} + +// PublicIPSpec defines the inputs to create an Azure public IP address. +type PublicIPSpec struct { + Name string `json:"name"` } // LoadBalancer defines an Azure load balancer. diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index 67f42fb12b4..e32f79363d4 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -638,6 +638,11 @@ func (in *Network) DeepCopy() *Network { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) { *out = *in + if in.PublicIP != nil { + in, out := &in.PublicIP, &out.PublicIP + *out = new(PublicIPSpec) + **out = **in + } in.Vnet.DeepCopyInto(&out.Vnet) if in.Subnets != nil { in, out := &in.Subnets, &out.Subnets @@ -693,6 +698,21 @@ func (in *PublicIP) DeepCopy() *PublicIP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicIPSpec) DeepCopyInto(out *PublicIPSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicIPSpec. +func (in *PublicIPSpec) DeepCopy() *PublicIPSpec { + if in == nil { + return nil + } + out := new(PublicIPSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RouteTable) DeepCopyInto(out *RouteTable) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml index ce18b42ce0d..4d6638deec0 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml @@ -465,6 +465,16 @@ spec: description: NetworkSpec encapsulates all things related to Azure network. properties: + publicIp: + description: PublicIP is the public IP to attach to the Azure + load balanacer. If name is not set in the spec, one will be + generated. + properties: + name: + type: string + required: + - name + type: object subnets: description: Subnets is the configuration for the control-plane subnet and the node subnet. @@ -798,6 +808,8 @@ spec: type: string name: type: string + required: + - name type: object apiServerLb: description: APIServerLB is the Kubernetes API server load balancer. diff --git a/controllers/azurecluster_reconciler.go b/controllers/azurecluster_reconciler.go index 58ba09b63ca..e6153be27d8 100644 --- a/controllers/azurecluster_reconciler.go +++ b/controllers/azurecluster_reconciler.go @@ -154,7 +154,7 @@ func (r *azureClusterReconciler) Reconcile(ctx context.Context) error { } publicIPSpec := &publicips.Spec{ - Name: r.scope.Network().APIServerIP.Name, + Name: r.scope.AzureCluster.Spec.NetworkSpec.PublicIP.Name, DNSName: r.scope.Network().APIServerIP.DNSName, } if err := r.publicIPSvc.Reconcile(ctx, publicIPSpec); err != nil { @@ -322,6 +322,8 @@ func (r *azureClusterReconciler) createOrUpdateNetworkAPIServerIP() error { return errors.Wrapf(err, "failed to write hash sum for api server ip") } r.scope.Network().APIServerIP.Name = azure.GeneratePublicIPName(r.scope.Name(), fmt.Sprintf("%x", h.Sum32())) + } else { + r.scope.Network().APIServerIP.Name = r.scope.AzureCluster.Spec.NetworkSpec.PublicIP.Name } r.scope.Network().APIServerIP.DNSName = r.scope.GenerateFQDN() diff --git a/templates/cluster-template-external-cloud-provider.yaml b/templates/cluster-template-external-cloud-provider.yaml index c72145a4264..76ae7506695 100644 --- a/templates/cluster-template-external-cloud-provider.yaml +++ b/templates/cluster-template-external-cloud-provider.yaml @@ -25,6 +25,8 @@ metadata: spec: location: ${AZURE_LOCATION} networkSpec: + publicIp: + name: ${AZURE_IP_NAME} vnet: name: ${AZURE_VNET_NAME} resourceGroup: ${AZURE_RESOURCE_GROUP} diff --git a/templates/cluster-template.yaml b/templates/cluster-template.yaml index 0e9cff846bc..c799ec6cab4 100644 --- a/templates/cluster-template.yaml +++ b/templates/cluster-template.yaml @@ -25,6 +25,8 @@ metadata: spec: location: ${AZURE_LOCATION} networkSpec: + publicIp: + name: ${AZURE_IP_NAME} vnet: name: ${AZURE_VNET_NAME} resourceGroup: ${AZURE_RESOURCE_GROUP} diff --git a/templates/flavors/default/kustomization.yaml b/templates/flavors/default/kustomization.yaml index 64f1b8bc5d7..7d5e2a1304f 100644 --- a/templates/flavors/default/kustomization.yaml +++ b/templates/flavors/default/kustomization.yaml @@ -2,3 +2,6 @@ namespace: default resources: - ../base - machine-deployment.yaml + +patchesStrategicMerge: +- patch_ip.yaml diff --git a/templates/flavors/default/patch_ip.yaml b/templates/flavors/default/patch_ip.yaml new file mode 100644 index 00000000000..477adef35ed --- /dev/null +++ b/templates/flavors/default/patch_ip.yaml @@ -0,0 +1,9 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3 +kind: AzureCluster +metadata: + name: ${CLUSTER_NAME} + namespace: default +spec: + networkSpec: + publicIp: + name: ${AZURE_IP_NAME} \ No newline at end of file diff --git a/templates/test/cluster-template-prow-ci-version.yaml b/templates/test/cluster-template-prow-ci-version.yaml index 21a954454e1..21bf311683f 100644 --- a/templates/test/cluster-template-prow-ci-version.yaml +++ b/templates/test/cluster-template-prow-ci-version.yaml @@ -28,6 +28,8 @@ spec: jobName: ${JOB_NAME} location: ${AZURE_LOCATION} networkSpec: + publicIp: + name: ${AZURE_IP_NAME} vnet: name: ${AZURE_VNET_NAME} resourceGroup: ${AZURE_RESOURCE_GROUP} diff --git a/templates/test/cluster-template-prow.yaml b/templates/test/cluster-template-prow.yaml index 9cecb276a91..516894e6ad3 100644 --- a/templates/test/cluster-template-prow.yaml +++ b/templates/test/cluster-template-prow.yaml @@ -28,6 +28,8 @@ spec: jobName: ${JOB_NAME} location: ${AZURE_LOCATION} networkSpec: + publicIp: + name: ${AZURE_IP_NAME} vnet: name: ${AZURE_VNET_NAME} resourceGroup: ${AZURE_RESOURCE_GROUP}