diff --git a/api/v1beta1/azuremanagedcontrolplane_default.go b/api/v1beta1/azuremanagedcontrolplane_default.go index 9cae98ad80f..9e2940f64fc 100644 --- a/api/v1beta1/azuremanagedcontrolplane_default.go +++ b/api/v1beta1/azuremanagedcontrolplane_default.go @@ -164,3 +164,9 @@ func (m *AzureManagedControlPlane) setDefaultOIDCIssuerProfile() { m.Spec.OIDCIssuerProfile.Enabled = ptr.To(false) } } + +func (m *AzureManagedControlPlane) setDefaultDNSPrefix() { + if m.Spec.DNSPrefix == nil { + m.Spec.DNSPrefix = ptr.To(m.Name) + } +} diff --git a/api/v1beta1/azuremanagedcontrolplane_types.go b/api/v1beta1/azuremanagedcontrolplane_types.go index e417a56b9fc..5129aafc199 100644 --- a/api/v1beta1/azuremanagedcontrolplane_types.go +++ b/api/v1beta1/azuremanagedcontrolplane_types.go @@ -213,6 +213,11 @@ type AzureManagedControlPlaneSpec struct { // OIDCIssuerProfile is the OIDC issuer profile of the Managed Cluster. // +optional OIDCIssuerProfile *OIDCIssuerProfile `json:"oidcIssuerProfile,omitempty"` + + // DNSPrefix allows the user to customize dns prefix. + // Immutable. + // +optional + DNSPrefix *string `json:"dnsPrefix,omitempty"` } // HTTPProxyConfig is the HTTP proxy configuration for the cluster. diff --git a/api/v1beta1/azuremanagedcontrolplane_webhook.go b/api/v1beta1/azuremanagedcontrolplane_webhook.go index df5f6b26456..5c8ea83a544 100644 --- a/api/v1beta1/azuremanagedcontrolplane_webhook.go +++ b/api/v1beta1/azuremanagedcontrolplane_webhook.go @@ -102,6 +102,7 @@ func (mw *azureManagedControlPlaneWebhook) Default(ctx context.Context, obj runt m.setDefaultSku() m.setDefaultAutoScalerProfile() m.setDefaultOIDCIssuerProfile() + m.setDefaultDNSPrefix() return nil } @@ -240,6 +241,14 @@ func (mw *azureManagedControlPlaneWebhook) ValidateUpdate(ctx context.Context, o } } + if err := webhookutils.ValidateImmutable( + field.NewPath("Spec", "DNSPrefix"), + m.Spec.DNSPrefix, + old.Spec.DNSPrefix, + ); err != nil { + allErrs = append(allErrs, err) + } + // Consider removing this once moves out of preview // Updating outboundType after cluster creation (PREVIEW) // https://learn.microsoft.com/en-us/azure/aks/egress-outboundtype#updating-outboundtype-after-cluster-creation-preview @@ -290,6 +299,7 @@ func (m *AzureManagedControlPlane) Validate(cli client.Client) error { m.validateAutoScalerProfile, m.validateIdentity, m.validateNetworkPluginMode, + m.validateDNSPrefix, } var errs []error @@ -302,6 +312,26 @@ func (m *AzureManagedControlPlane) Validate(cli client.Client) error { return kerrors.NewAggregate(errs) } +func (m *AzureManagedControlPlane) validateDNSPrefix(_ client.Client) error { + if m.Spec.DNSPrefix == nil { + return nil + } + + // Regex pattern for DNS prefix validation + // 1. Between 1 and 54 characters long: {1,54} + // 2. Alphanumerics and hyphens: [a-zA-Z0-9-] + // 3. Start and end with alphanumeric: ^[a-zA-Z0-9].*[a-zA-Z0-9]$ + pattern := `^[a-zA-Z0-9][a-zA-Z0-9-]{0,52}[a-zA-Z0-9]$` + regex := regexp.MustCompile(pattern) + if regex.MatchString(ptr.Deref(m.Spec.DNSPrefix, "")) { + return nil + } + allErrs := field.ErrorList{ + field.Invalid(field.NewPath("Spec", "DNSPrefix"), *m.Spec.DNSPrefix, "DNSPrefix is invalid, does not match regex: "+pattern), + } + return kerrors.NewAggregate(allErrs.ToAggregate().Errors()) +} + // validateVersion validates the Kubernetes version. func (m *AzureManagedControlPlane) validateVersion(_ client.Client) error { if !kubeSemver.MatchString(m.Spec.Version) { diff --git a/api/v1beta1/azuremanagedcontrolplane_webhook_test.go b/api/v1beta1/azuremanagedcontrolplane_webhook_test.go index 438bcbf0266..7569c9b4cc7 100644 --- a/api/v1beta1/azuremanagedcontrolplane_webhook_test.go +++ b/api/v1beta1/azuremanagedcontrolplane_webhook_test.go @@ -59,6 +59,8 @@ func TestDefaultingWebhook(t *testing.T) { g.Expect(amcp.Spec.SKU.Tier).To(Equal(FreeManagedControlPlaneTier)) g.Expect(amcp.Spec.Identity.Type).To(Equal(ManagedControlPlaneIdentityTypeSystemAssigned)) g.Expect(*amcp.Spec.OIDCIssuerProfile.Enabled).To(BeFalse()) + g.Expect(amcp.Spec.DNSPrefix).ToNot(BeNil()) + g.Expect(*amcp.Spec.DNSPrefix).To(Equal(amcp.Name)) t.Logf("Testing amcp defaulting webhook with baseline") netPlug := "kubenet" @@ -76,6 +78,7 @@ func TestDefaultingWebhook(t *testing.T) { amcp.Spec.OIDCIssuerProfile = &OIDCIssuerProfile{ Enabled: ptr.To(true), } + amcp.Spec.DNSPrefix = ptr.To("test-prefix") err = mcpw.Default(context.Background(), amcp) g.Expect(err).NotTo(HaveOccurred()) @@ -89,7 +92,8 @@ func TestDefaultingWebhook(t *testing.T) { g.Expect(amcp.Spec.VirtualNetwork.Subnet.Name).To(Equal("fooSubnetName")) g.Expect(amcp.Spec.SKU.Tier).To(Equal(PaidManagedControlPlaneTier)) g.Expect(*amcp.Spec.OIDCIssuerProfile.Enabled).To(BeTrue()) - + g.Expect(amcp.Spec.DNSPrefix).ToNot(BeNil()) + g.Expect(*amcp.Spec.DNSPrefix).To(Equal("test-prefix")) t.Logf("Testing amcp defaulting webhook with overlay") amcp = &AzureManagedControlPlane{ ObjectMeta: metav1.ObjectMeta{ @@ -778,6 +782,76 @@ func TestAzureManagedControlPlane_ValidateCreate(t *testing.T) { wantErr: true, errorLen: 1, }, + { + name: "Testing inValid DNSPrefix for starting with invalid characters", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("-thisi$"), + Version: "v1.17.8", + }, + }, + wantErr: true, + }, + { + name: "Testing inValid DNSPrefix with more then 54 characters", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("thisisaverylong$^clusternameconsistingofmorethan54characterswhichshouldbeinvalid"), + Version: "v1.17.8", + }, + }, + wantErr: true, + }, + { + name: "Testing inValid DNSPrefix with underscore", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("no_underscore"), + Version: "v1.17.8", + }, + }, + wantErr: true, + }, + { + name: "Testing inValid DNSPrefix with special characters", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("no-dollar$@%"), + Version: "v1.17.8", + }, + }, + wantErr: true, + }, + { + name: "Testing Valid DNSPrefix with hyphen characters", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("hyphen-allowed"), + Version: "v1.17.8", + }, + }, + wantErr: false, + }, + { + name: "Testing Valid DNSPrefix with hyphen characters", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("palette-test07"), + Version: "v1.17.8", + }, + }, + wantErr: false, + }, + { + name: "Testing valid DNSPrefix ", + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("thisisavlerylongclu7l0sternam3leconsistingofmorethan54"), + Version: "v1.17.8", + }, + }, + wantErr: false, + }, { name: "invalid name with microsoft", amcp: &AzureManagedControlPlane{ @@ -1609,6 +1683,86 @@ func TestAzureManagedControlPlane_ValidateUpdate(t *testing.T) { }, wantErr: false, }, + { + name: "AzureManagedControlPlane DNSPrefix is immutable error", + oldAMCP: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("capz-aks-1"), + Version: "v1.18.0", + }, + }, + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("capz-aks"), + Version: "v1.18.0", + }, + }, + wantErr: true, + }, + { + name: "AzureManagedControlPlane DNSPrefix is immutable no error", + oldAMCP: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("capz-aks"), + Version: "v1.18.0", + }, + }, + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("capz-aks"), + Version: "v1.18.0", + }, + }, + wantErr: false, + }, + { + name: "AzureManagedControlPlane DNSPrefix is immutable error nil -> capz-aks", + oldAMCP: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: nil, + Version: "v1.18.0", + }, + }, + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To("capz-aks"), + Version: "v1.18.0", + }, + }, + wantErr: true, + }, + { + name: "AzureManagedControlPlane DNSPrefix is immutable error nil -> empty", + oldAMCP: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: nil, + Version: "v1.18.0", + }, + }, + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: ptr.To(""), + Version: "v1.18.0", + }, + }, + wantErr: true, + }, + { + name: "AzureManagedControlPlane DNSPrefix is immutable no error nil -> nil", + oldAMCP: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: nil, + Version: "v1.18.0", + }, + }, + amcp: &AzureManagedControlPlane{ + Spec: AzureManagedControlPlaneSpec{ + DNSPrefix: nil, + Version: "v1.18.0", + }, + }, + wantErr: false, + }, } client := mockClient{ReturnError: false} for _, tc := range tests { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index d9270d475ba..6e1797cc41d 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1247,6 +1247,11 @@ func (in *AzureManagedControlPlaneSpec) DeepCopyInto(out *AzureManagedControlPla *out = new(OIDCIssuerProfile) (*in).DeepCopyInto(*out) } + if in.DNSPrefix != nil { + in, out := &in.DNSPrefix, &out.DNSPrefix + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureManagedControlPlaneSpec. diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go index 9be7e235e45..87b95528d9d 100644 --- a/azure/scope/managedcontrolplane.go +++ b/azure/scope/managedcontrolplane.go @@ -485,6 +485,7 @@ func (s *ManagedControlPlaneScope) ManagedClusterSpec() azure.ResourceSpecGetter Identity: s.ControlPlane.Spec.Identity, KubeletUserAssignedIdentity: s.ControlPlane.Spec.KubeletUserAssignedIdentity, NetworkPluginMode: s.ControlPlane.Spec.NetworkPluginMode, + DNSPrefix: s.ControlPlane.Spec.DNSPrefix, } if s.ControlPlane.Spec.SSHPublicKey != nil { diff --git a/azure/services/managedclusters/spec.go b/azure/services/managedclusters/spec.go index b75f0335a0d..4cd3362dad7 100644 --- a/azure/services/managedclusters/spec.go +++ b/azure/services/managedclusters/spec.go @@ -122,6 +122,9 @@ type ManagedClusterSpec struct { // OIDCIssuerProfile is the OIDC issuer profile of the Managed Cluster. OIDCIssuerProfile *OIDCIssuerProfile + + // DNSPrefix allows the user to customize dns prefix. + DNSPrefix *string } // HTTPProxyConfig is the HTTP proxy configuration for the cluster. @@ -326,7 +329,7 @@ func (s *ManagedClusterSpec) Parameters(ctx context.Context, existing interface{ Properties: &armcontainerservice.ManagedClusterProperties{ NodeResourceGroup: &s.NodeResourceGroup, EnableRBAC: ptr.To(true), - DNSPrefix: &s.Name, + DNSPrefix: s.DNSPrefix, KubernetesVersion: &s.Version, ServicePrincipalProfile: &armcontainerservice.ManagedClusterServicePrincipalProfile{ diff --git a/azure/services/managedclusters/spec_test.go b/azure/services/managedclusters/spec_test.go index 9b3e69d9d11..796f8cf19a3 100644 --- a/azure/services/managedclusters/spec_test.go +++ b/azure/services/managedclusters/spec_test.go @@ -589,7 +589,6 @@ func getSampleManagedCluster() armcontainerservice.ManagedCluster { return armcontainerservice.ManagedCluster{ Properties: &armcontainerservice.ManagedClusterProperties{ KubernetesVersion: ptr.To("v1.22.0"), - DNSPrefix: ptr.To("test-managedcluster"), AgentPoolProfiles: []*armcontainerservice.ManagedClusterAgentPoolProfile{ { Name: ptr.To("test-agentpool-0"), diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml index 4d1bac26596..a5a9c97a2b7 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azuremanagedcontrolplanes.yaml @@ -239,6 +239,9 @@ spec: - host - port type: object + dnsPrefix: + description: DNSPrefix allows the user to customize dns prefix. Immutable. + type: string dnsServiceIP: description: DNSServiceIP is an IP address assigned to the Kubernetes DNS service. It must be within the Kubernetes service address range