diff --git a/api/v1alpha1/frontdoorbackend_types.go b/api/v1alpha1/frontdoorbackend_types.go new file mode 100644 index 00000000..a3c63786 --- /dev/null +++ b/api/v1alpha1/frontdoorbackend_types.go @@ -0,0 +1,229 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + FrontDoorBackendKind = "FrontDoorBackend" +) + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,categories={fleet-networking},shortName=fdb +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=`.spec.profile.name`,name="Profile",type=string +// +kubebuilder:printcolumn:JSONPath=`.spec.backend.name`,name="Backend",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=='Accepted')].status`,name="Is-Accepted",type=string +// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date + +// FrontDoorBackend is the AFD-side counterpart to TrafficManagerBackend: it +// binds a fleet-scoped ServiceImport (specifically, its L7-FrontDoor exports +// carrying PrivateLinkServiceResourceID — see InternalServiceExportSpec) to a +// FrontDoorProfile, producing an Azure Front Door OriginGroup with one Origin +// per qualifying member-cluster export. +// +// The shape intentionally mirrors TrafficManagerBackend 1:1 so operators +// authoring both surfaces have a single mental model: +// +// - Spec.Profile → parent FrontDoorProfile (same namespace) +// - Spec.Backend → ServiceImport (same namespace) +// - Spec.Weight → aggregate traffic weight, distributed across +// per-export weights the same way as ATM (see the +// TrafficManagerBackendSpec.Weight formula). +// +// Design references: +// - docs/first-party/001-afd-global-load-balancing.md §4 (backend model) +// - docs/first-party/002-afd-implementation-plan.md §6 (reconciler) +// +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) < 64",message="metadata.name max length is 63" +type FrontDoorBackend struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // The desired state of FrontDoorBackend. + Spec FrontDoorBackendSpec `json:"spec"` + + // The observed status of FrontDoorBackend. + // +optional + Status FrontDoorBackendStatus `json:"status,omitempty"` +} + +// FrontDoorBackendSpec describes which FrontDoorProfile a ServiceImport is +// attached to, and how much aggregate traffic it should receive. Profile and +// Backend are immutable — changing them would require recreating the +// underlying OriginGroup, which loses live-traffic guarantees. +type FrontDoorBackendSpec struct { + // Which FrontDoorProfile the backend should be attached to. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.profile is immutable" + Profile FrontDoorProfileRef `json:"profile"` + + // The reference to a backend. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.backend is immutable" + Backend FrontDoorBackendRef `json:"backend"` + + // The aggregate weight of origins behind the serviceImport under the + // generated Front Door OriginGroup. Possible values are from 0 to 1000. + // Semantics match TrafficManagerBackendSpec.Weight: the value is + // distributed across each cluster's exports proportionally to the + // per-export weight surfaced on the ServiceExport. If weight is set to + // 0, all origins behind the serviceImport will be removed from the + // OriginGroup (effectively draining traffic). + // +optional + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1000 + // +kubebuilder:default=1 + Weight *int64 `json:"weight,omitempty"` +} + +// FrontDoorProfileRef is a reference to a FrontDoorProfile in the same +// namespace as the FrontDoorBackend. Modeled as a struct (not a bare string) +// to leave room for cross-namespace / cross-tenant refs later without a +// breaking API change. +type FrontDoorProfileRef struct { + // Name is the name of the referenced FrontDoorProfile. + // +required + Name string `json:"name"` +} + +// FrontDoorBackendRef is the reference to a backend. Currently only +// ServiceImport is supported; the struct wrapper anticipates additional +// backend types (e.g. direct PrivateLinkServiceResourceID refs) without a +// breaking change. +type FrontDoorBackendRef struct { + // Name is the reference to the ServiceImport in the same namespace as + // the FrontDoorBackend object. + // +required + Name string `json:"name"` +} + +// FrontDoorOriginStatus captures the status of a single Azure Front Door +// Origin created under the FrontDoorProfile for this backend. +type FrontDoorOriginStatus struct { + // Name of the origin (as created inside the AFD OriginGroup). + // +required + Name string `json:"name"` + + // ResourceID is the fully qualified Azure resource Id of the origin. + // Ex - /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{profile}/originGroups/{group}/origins/{name} + // +optional + ResourceID string `json:"resourceID,omitempty"` + + // PrivateLinkServiceResourceID is the PLS this origin was wired to + // (see InternalServiceExportSpec.PrivateLinkServiceResourceID). + // +optional + PrivateLinkServiceResourceID *string `json:"privateLinkServiceResourceID,omitempty"` + + // Weight is the effective weight of this origin, after the aggregate + // FrontDoorBackendSpec.Weight has been distributed across per-export + // weights. Semantics parallel TrafficManagerEndpointStatus.Weight. + // +optional + Weight *int64 `json:"weight,omitempty"` + + // From is where the origin's underlying export was sourced. + // +optional + From *FromCluster `json:"from,omitempty"` +} + +// FrontDoorBackendStatus reflects what the reconciler has actually +// programmed on the Azure Front Door profile. OriginGroupResourceID lets +// consumers (e.g. FrontDoorRoute in a later phase) key off a stable ID +// without re-deriving it from the profile + backend names. +type FrontDoorBackendStatus struct { + // OriginGroupResourceID is the fully qualified Azure resource Id of + // the Origin Group created for this backend. Empty while the + // backend is still being accepted. + // +optional + OriginGroupResourceID string `json:"originGroupResourceID,omitempty"` + + // Origins contains a list of accepted Azure Front Door origins that + // are created or updated under the generated OriginGroup. + // +optional + Origins []FrontDoorOriginStatus `json:"origins,omitempty"` + + // Current backend status. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// FrontDoorBackendConditionType is a type of condition associated with a +// FrontDoorBackendStatus. Mirrors TrafficManagerBackendConditionType so +// consumers can reuse condition-handling code across the two backends. +type FrontDoorBackendConditionType string + +// FrontDoorBackendConditionReason defines the set of reasons that explain +// why a particular backend has been raised. +type FrontDoorBackendConditionReason string + +const ( + // FrontDoorBackendConditionAccepted indicates whether origins have + // been created or updated for the profile. This does not indicate + // whether or not the configuration has been propagated to the AFD + // data plane (POP rollout is asynchronous and observed via ARM only + // after the fact). + // + // Possible reasons for this condition to be True are: + // + // * "Accepted" + // + // Possible reasons for this condition to be False are: + // + // * "Invalid" + // * "Conflict" (a TrafficManagerBackend already claims the same + // ServiceImport — the AFD/ATM coexistence guard rejects the + // duplicate to avoid split-brain export ownership) + // + // Possible reasons for this condition to be Unknown are: + // + // * "Pending" + // + FrontDoorBackendConditionAccepted FrontDoorBackendConditionType = "Accepted" + + // FrontDoorBackendReasonAccepted is used with the "Accepted" + // condition when the condition is True. + FrontDoorBackendReasonAccepted FrontDoorBackendConditionReason = "Accepted" + + // FrontDoorBackendReasonInvalid is used with the "Accepted" + // condition when one or more origin references have an invalid or + // unsupported configuration (e.g. an export with ExportMode != + // L7-FrontDoor, or a missing PrivateLinkServiceResourceID). + FrontDoorBackendReasonInvalid FrontDoorBackendConditionReason = "Invalid" + + // FrontDoorBackendReasonConflict is used with the "Accepted" + // condition when the AFD/ATM coexistence guard rejects a backend + // because a TrafficManagerBackend already owns the same + // ServiceImport. See docs/first-party/002-afd-implementation-plan.md + // §7 for the ownership rules. + FrontDoorBackendReasonConflict FrontDoorBackendConditionReason = "Conflict" + + // FrontDoorBackendReasonPending is used with the "Accepted" condition + // when creating or updating origins hits a transient error; the + // controller keeps retrying. + FrontDoorBackendReasonPending FrontDoorBackendConditionReason = "Pending" +) + +// +kubebuilder:object:root=true + +// FrontDoorBackendList contains a list of FrontDoorBackend. +type FrontDoorBackendList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + // +listType=set + Items []FrontDoorBackend `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FrontDoorBackend{}, &FrontDoorBackendList{}) +} diff --git a/api/v1alpha1/frontdoorcustomdomain_types.go b/api/v1alpha1/frontdoorcustomdomain_types.go new file mode 100644 index 00000000..cd5e5924 --- /dev/null +++ b/api/v1alpha1/frontdoorcustomdomain_types.go @@ -0,0 +1,229 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + FrontDoorCustomDomainKind = "FrontDoorCustomDomain" +) + +// FrontDoorTLSMode selects how the TLS certificate for a custom domain is sourced. +type FrontDoorTLSMode string + +const ( + // FrontDoorTLSModeManaged uses an AFD-managed certificate that is issued + // and renewed automatically by Azure. + FrontDoorTLSModeManaged FrontDoorTLSMode = "Managed" + + // FrontDoorTLSModeBYOC binds a customer-supplied certificate stored in + // Azure Key Vault. Requires KeyVaultCertificate to be set. + // + // NOTE (POC, cb02d14): reserved. The controller currently only implements + // the Managed path; BYOC reconciliation is deferred. Cross-field CEL + // validation on FrontDoorTLSConfig accepts BYOC + keyVaultCertificate + // at admission time, but the reconciler surfaces + // Programmed=False, Reason=TLSFailed until Phase 4 adds the Key Vault + // binding. Tracked in docs/first-party/002-afd-implementation-plan.md + // §9 risks (breadcrumb D3). + FrontDoorTLSModeBYOC FrontDoorTLSMode = "BYOC" +) + +// Design note (traffic vs. ownership): a validated FrontDoorCustomDomain +// only proves ownership and provisions the AFD-side custom-domain +// resource. It does NOT front any traffic on its own. Attaching a +// custom domain to an AFD route (so end-user requests actually resolve +// through it) is the FrontDoorBackend controller's job, which arrives +// in Phase 4. Cross-reference: +// docs/first-party/002-afd-implementation-plan.md §3.3. + +// FrontDoorDomainValidationState is the current state of DNS-based ownership +// validation of a custom domain, mirroring the AFD resource provider states. +type FrontDoorDomainValidationState string + +const ( + FrontDoorDomainValidationStatePending FrontDoorDomainValidationState = "Pending" + FrontDoorDomainValidationStateApproved FrontDoorDomainValidationState = "Approved" + FrontDoorDomainValidationStateRejected FrontDoorDomainValidationState = "Rejected" + FrontDoorDomainValidationStateTimedOut FrontDoorDomainValidationState = "TimedOut" + FrontDoorDomainValidationStateInternalError FrontDoorDomainValidationState = "InternalError" + FrontDoorDomainValidationStateSubmitting FrontDoorDomainValidationState = "Submitting" + FrontDoorDomainValidationStateRefreshing FrontDoorDomainValidationState = "RefreshingValidationToken" + FrontDoorDomainValidationStateUnknown FrontDoorDomainValidationState = "Unknown" +) + +// FrontDoorProfileReference references a FrontDoorProfile by name in the same +// namespace as the FrontDoorCustomDomain. +type FrontDoorProfileReference struct { + // Name of the target FrontDoorProfile. Must exist in the same namespace as + // this FrontDoorCustomDomain (per breadcrumb D4: same-namespace only). + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` +} + +// FrontDoorKeyVaultCertificate references a certificate stored in an Azure +// Key Vault. Used only when TLS.Mode is "BYOC". +type FrontDoorKeyVaultCertificate struct { + // VaultURI is the base URI of the Key Vault, e.g. https://myvault.vault.azure.net. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://[a-zA-Z0-9-]+\.vault\.azure\.net/?$` + VaultURI string `json:"vaultURI"` + + // CertificateName is the name of the certificate in the Key Vault. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=127 + CertificateName string `json:"certificateName"` + + // Version optionally pins a specific certificate version. When omitted, + // the controller tracks the latest version and re-binds on rotation. + // +optional + Version *string `json:"version,omitempty"` +} + +// FrontDoorTLSConfig defines the TLS configuration for a custom domain. +// +kubebuilder:validation:XValidation:rule="self.mode != 'BYOC' || has(self.keyVaultCertificate)",message="keyVaultCertificate is required when mode is BYOC" +// +kubebuilder:validation:XValidation:rule="self.mode != 'Managed' || !has(self.keyVaultCertificate)",message="keyVaultCertificate must not be set when mode is Managed" +type FrontDoorTLSConfig struct { + // Mode selects the source of the TLS certificate. + // +required + // +kubebuilder:validation:Enum=Managed;BYOC + Mode FrontDoorTLSMode `json:"mode"` + + // KeyVaultCertificate references the certificate to bind when Mode is BYOC. + // +optional + KeyVaultCertificate *FrontDoorKeyVaultCertificate `json:"keyVaultCertificate,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,categories={fleet-networking},shortName=afdcd +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=`.spec.hostname`,name="Hostname",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.validationState`,name="Validation",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=='Programmed')].status`,name="Is-Programmed",type=string +// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date + +// FrontDoorCustomDomain represents a custom domain attached to a FrontDoorProfile, +// including the DNS-based ownership validation and the TLS binding. +// https://learn.microsoft.com/en-us/azure/frontdoor/domain +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) < 64",message="metadata.name max length is 63" +type FrontDoorCustomDomain struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // The desired state of FrontDoorCustomDomain. + Spec FrontDoorCustomDomainSpec `json:"spec"` + + // The observed status of FrontDoorCustomDomain. + // +optional + Status FrontDoorCustomDomainStatus `json:"status,omitempty"` +} + +// FrontDoorCustomDomainSpec defines the desired state of FrontDoorCustomDomain. +type FrontDoorCustomDomainSpec struct { + // ProfileRef references the FrontDoorProfile that owns this custom domain. + // The referenced profile must exist in the same namespace as this resource. + // Immutable after creation. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="profileRef is immutable" + ProfileRef FrontDoorProfileReference `json:"profileRef"` + + // Hostname is the fully qualified custom domain name (e.g. www.contoso.com). + // Immutable after creation. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="hostname is immutable" + Hostname string `json:"hostname"` + + // TLS configures the certificate binding for this custom domain. + // +required + TLS FrontDoorTLSConfig `json:"tls"` +} + +// FrontDoorCustomDomainStatus defines the observed state of FrontDoorCustomDomain. +type FrontDoorCustomDomainStatus struct { + // ResourceID is the fully qualified Azure resource ID of the custom domain. + // Example: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{profile}/customDomains/{name} + // +optional + ResourceID string `json:"resourceID,omitempty"` + + // ValidationState is the current DNS ownership validation state reported by Azure. + // +optional + ValidationState FrontDoorDomainValidationState `json:"validationState,omitempty"` + + // DNSValidationToken is the token that must be published as a TXT record on + // the customer's DNS zone to prove ownership. The expected TXT record name + // is `_dnsauth.`, and the value is this token. + // +optional + DNSValidationToken *string `json:"dnsValidationToken,omitempty"` + + // DNSValidationExpiry is the time at which the current validation token + // expires. Azure issues a new token periodically; the controller refreshes + // status before expiry. + // +optional + DNSValidationExpiry *metav1.Time `json:"dnsValidationExpiry,omitempty"` + + // Current custom domain status. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// FrontDoorCustomDomainConditionType is a type of condition associated with a +// FrontDoorCustomDomain. +type FrontDoorCustomDomainConditionType string + +// FrontDoorCustomDomainConditionReason defines the set of reasons that explain +// why a particular condition type has been raised. +type FrontDoorCustomDomainConditionReason string + +const ( + // FrontDoorCustomDomainConditionProgrammed indicates whether the custom + // domain resource has been programmed in AFD (created + validated + bound). + // + // Positive-polarity summary condition; always present on the resource with + // ObservedGeneration set. + // + // True reasons: "Programmed". + // False reasons: "Invalid", "ProfileNotReady", "ValidationFailed", "TLSFailed", "AzureError". + // Unknown reasons: "Pending", "AwaitingDNSValidation". + FrontDoorCustomDomainConditionProgrammed FrontDoorCustomDomainConditionType = "Programmed" + + FrontDoorCustomDomainReasonProgrammed FrontDoorCustomDomainConditionReason = "Programmed" + FrontDoorCustomDomainReasonInvalid FrontDoorCustomDomainConditionReason = "Invalid" + FrontDoorCustomDomainReasonProfileNotReady FrontDoorCustomDomainConditionReason = "ProfileNotReady" + FrontDoorCustomDomainReasonAwaitingDNSValidation FrontDoorCustomDomainConditionReason = "AwaitingDNSValidation" + FrontDoorCustomDomainReasonValidationFailed FrontDoorCustomDomainConditionReason = "ValidationFailed" + FrontDoorCustomDomainReasonTLSFailed FrontDoorCustomDomainConditionReason = "TLSFailed" + FrontDoorCustomDomainReasonAzureError FrontDoorCustomDomainConditionReason = "AzureError" + FrontDoorCustomDomainReasonPending FrontDoorCustomDomainConditionReason = "Pending" +) + +// +kubebuilder:object:root=true + +// FrontDoorCustomDomainList contains a list of FrontDoorCustomDomain. +type FrontDoorCustomDomainList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + // +listType=set + Items []FrontDoorCustomDomain `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FrontDoorCustomDomain{}, &FrontDoorCustomDomainList{}) +} diff --git a/api/v1alpha1/frontdoorprofile_types.go b/api/v1alpha1/frontdoorprofile_types.go new file mode 100644 index 00000000..840303e7 --- /dev/null +++ b/api/v1alpha1/frontdoorprofile_types.go @@ -0,0 +1,260 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + FrontDoorProfileKind = "FrontDoorProfile" +) + +// FrontDoorProfileSkuName defines the SKU of the Azure Front Door profile. +// +// Only Premium is supported (see docs/first-party/001-afd-global-load-balancing.md +// §2.3 and docs/first-party/003-pre-implementation-checklist.md §1.2). Private +// Link origins — the SFI-NS253 cornerstone that makes AFD viable as a +// first-party GLB surface — are Premium-only, so Standard would produce an +// installation that could not satisfy the compliance regime this feature +// exists to serve. Restricting the enum at admission time gives tenants an +// immediate, unambiguous rejection instead of a surprise runtime condition +// hours later at backend creation. +// +// Note there is no Location field on FrontDoorProfileSpec: AFD is a global +// service and the RP rejects any Location other than "Global", so the +// controller sets Location internally rather than exposing a single-valued +// CR field. +type FrontDoorProfileSkuName string + +const ( + // FrontDoorProfileSkuPremium is the only supported SKU value; see + // FrontDoorProfileSkuName for the SFI-NS253 rationale. + FrontDoorProfileSkuPremium FrontDoorProfileSkuName = "Premium_AzureFrontDoor" +) + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,categories={fleet-networking},shortName=afdp +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=`.status.endpointHostname`,name="Endpoint",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=='Programmed')].status`,name="Is-Programmed",type=string +// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date + +// FrontDoorProfile manages an Azure Front Door profile and its default endpoint +// using the cloud-native (Kubernetes) API. It is the L7 counterpart to +// TrafficManagerProfile. +// https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) < 64",message="metadata.name max length is 63" +// +kubebuilder:validation:XValidation:rule="self.spec.complianceMode != 'SFI-NS253' || has(self.spec.wafPolicy)",message="spec.wafPolicy is required when spec.complianceMode is SFI-NS253" +type FrontDoorProfile struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // The desired state of FrontDoorProfile. + Spec FrontDoorProfileSpec `json:"spec"` + + // The observed status of FrontDoorProfile. + // +optional + Status FrontDoorProfileStatus `json:"status,omitempty"` +} + +// FrontDoorProfileSpec defines the desired state of FrontDoorProfile. +type FrontDoorProfileSpec struct { + // ResourceGroup is the name of the Azure resource group in which the + // underlying Front Door profile will be created. Immutable after creation. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=90 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceGroup is immutable" + ResourceGroup string `json:"resourceGroup"` + + // Sku selects the Front Door SKU. Premium is the only supported value + // (see the FrontDoorProfileSkuName type comment for the SFI-NS253 + // rationale). Retained as an explicit field — even though the enum is + // currently single-valued — so future SKUs (if AFD ever ships a + // compliance-equivalent alternative) can be introduced additively + // without a schema break. Immutable after creation because AFD does + // not support in-place SKU upgrades on an existing profile. + // +required + // +kubebuilder:validation:Enum=Premium_AzureFrontDoor + // +kubebuilder:default=Premium_AzureFrontDoor + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="sku is immutable" + Sku FrontDoorProfileSkuName `json:"sku,omitempty"` + + // ComplianceMode declares the security/compliance regime this profile + // (and its backends) must satisfy. See docs/first-party/001-afd-global-load-balancing.md + // §2.1 for the design and docs/first-party/003-pre-implementation-checklist.md §1.2 + // for the operational requirements. + // + // Effects of the value: + // - "None" (default): no additional constraints beyond the structural + // ones. Suitable for dev/test tenants that don't need SFI-NS253 + // compliance, and lets the profile be created without a WAF policy + // attach (WAFPolicy stays optional). + // - "SFI-NS253": + // * spec.wafPolicy is REQUIRED (enforced by the cross-field CEL + // rule on this Spec — see below). + // * The FrontDoorProfile reconciler additionally verifies that the + // referenced WAF policy exists and is in Prevention mode + // (surfaced as Programmed=False with + // Reason=WAFPolicyNotFound / WAFPolicyNotInPreventionMode). + // * Every FrontDoorBackend that references this profile must have + // spec.privateLink.enabled = true (enforced in the backend + // reconciler, surfaced as Accepted=False, + // Reason=SFIComplianceViolation). + // + // Immutable after creation: switching a live profile out of SFI-NS253 + // would silently weaken guarantees the operator relied on when + // creating the profile — recreate the profile if the compliance regime + // legitimately needs to change. + // +optional + // +kubebuilder:validation:Enum=None;SFI-NS253 + // +kubebuilder:default=None + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="complianceMode is immutable" + ComplianceMode FrontDoorProfileComplianceMode `json:"complianceMode,omitempty"` + + // WAFPolicy attaches a Web Application Firewall policy to the profile. + // + // Required when ComplianceMode is "SFI-NS253"; optional otherwise (dev/test + // tenants may run without a WAF attach). The cross-field CEL rule below + // rejects any object that violates this at admission time so that a + // misconfigured SFI-NS253 profile never even reaches the reconciler. + // +optional + WAFPolicy *FrontDoorWAFPolicyRef `json:"wafPolicy,omitempty"` +} + +// FrontDoorProfileComplianceMode selects the security/compliance regime a +// FrontDoorProfile is subject to. See the ComplianceMode field on +// FrontDoorProfileSpec for the enforcement matrix. +type FrontDoorProfileComplianceMode string + +const ( + // FrontDoorProfileComplianceModeNone imposes no compliance-driven + // constraints beyond the structural ones (SKU enum, immutability, etc.). + FrontDoorProfileComplianceModeNone FrontDoorProfileComplianceMode = "None" + + // FrontDoorProfileComplianceModeSFINS253 enables the full SFI-NS253 + // enforcement described on the ComplianceMode field of + // FrontDoorProfileSpec: WAF required + Prevention mode + PrivateLink + // mandatory on all referencing backends. + FrontDoorProfileComplianceModeSFINS253 FrontDoorProfileComplianceMode = "SFI-NS253" +) + +// FrontDoorWAFPolicyRef references a Front Door Web Application Firewall +// policy that should be bound to the profile's default endpoint (and, in the +// future, its custom domains via a securityPolicies AFD resource). +// +// The reference is a raw ARM resource ID rather than a Kubernetes object +// reference because AFD WAF policies are typically pre-created and centrally +// managed (e.g. by a security team) and do not have a matching CRD in this +// repository today. +// +// An inline creation path was sketched in the design proposal +// (docs/first-party/002-afd-implementation-plan.md §3.1 — the `Inline` field +// on FrontDoorWAFPolicyRef) but is deliberately deferred: nearly all +// first-party services will reference a centrally-managed WAF policy, and +// exposing inline creation would duplicate what would ultimately be its own +// CRD (WAFPolicy) with its own reconciler. When/if inline creation lands, it +// will be added as an additional optional field on this struct without a +// schema break. +type FrontDoorWAFPolicyRef struct { + // ResourceID is the fully qualified ARM resource ID of an existing + // Microsoft.Network/frontdoorwebapplicationfirewallpolicies resource. + // + // Format: + // /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/frontdoorwebapplicationfirewallpolicies/{name} + // + // The policy may live in a different subscription/RG from the AFD + // profile; cross-subscription references are supported at the Azure + // level, subject to the AFD controller's managed identity having + // Microsoft.Network/frontDoorWebApplicationFirewallPolicies/read on + // the policy's scope. Cross-sub RBAC failures surface as + // Programmed=False, Reason=WAFPolicyNotFound with the exact ID in the + // message (per docs/first-party/002-afd-implementation-plan.md §11). + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\.Network/frontdoorwebapplicationfirewallpolicies/[^/]+$` + ResourceID string `json:"resourceID"` +} + +// FrontDoorProfileStatus defines the observed state of FrontDoorProfile. +type FrontDoorProfileStatus struct { + // ResourceID is the fully qualified Azure resource ID of the Front Door profile. + // Example: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{name} + // +optional + ResourceID string `json:"resourceID,omitempty"` + + // EndpointHostname is the default *.azurefd.net hostname assigned by Azure + // to the profile's default endpoint. Populated once the endpoint is programmed. + // +optional + EndpointHostname *string `json:"endpointHostname,omitempty"` + + // Current profile status. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// FrontDoorProfileConditionType is a type of condition associated with a +// FrontDoorProfile. This type should be used within the FrontDoorProfileStatus.Conditions field. +type FrontDoorProfileConditionType string + +// FrontDoorProfileConditionReason defines the set of reasons that explain why +// a particular profile condition type has been raised. +type FrontDoorProfileConditionReason string + +const ( + // FrontDoorProfileConditionProgrammed indicates whether the AFD profile and + // its default endpoint have been programmed in Azure. + // + // Positive-polarity summary condition; always present on the resource with + // ObservedGeneration set. + // + // True reasons: "Programmed". + // False reasons: "Invalid", "AzureError", "WAFPolicyNotFound", "WAFPolicyNotInPreventionMode". + // Unknown reasons: "Pending". + FrontDoorProfileConditionProgrammed FrontDoorProfileConditionType = "Programmed" + + FrontDoorProfileReasonProgrammed FrontDoorProfileConditionReason = "Programmed" + FrontDoorProfileReasonInvalid FrontDoorProfileConditionReason = "Invalid" + FrontDoorProfileReasonAzureError FrontDoorProfileConditionReason = "AzureError" + FrontDoorProfileReasonPending FrontDoorProfileConditionReason = "Pending" + + // FrontDoorProfileReasonWAFPolicyNotFound indicates that spec.wafPolicy + // references a policy ARM resource ID that the AFD controller cannot + // resolve (Azure returns NotFound, or the controller's identity lacks + // the required read permission on the policy's scope — the two failure + // modes are indistinguishable from the AFD RP's perspective, so both + // surface as this single reason; the exact ID is included in the + // condition message for diagnosis). + FrontDoorProfileReasonWAFPolicyNotFound FrontDoorProfileConditionReason = "WAFPolicyNotFound" + + // FrontDoorProfileReasonWAFPolicyNotInPreventionMode indicates that + // spec.wafPolicy resolves to a real policy but the policy's + // PolicySettings.Mode is not "Prevention". Only enforced when + // ComplianceMode == "SFI-NS253" (Detection mode is a valid choice for + // tenants with ComplianceMode="None" who want alerts without blocking). + FrontDoorProfileReasonWAFPolicyNotInPreventionMode FrontDoorProfileConditionReason = "WAFPolicyNotInPreventionMode" +) + +// +kubebuilder:object:root=true + +// FrontDoorProfileList contains a list of FrontDoorProfile. +type FrontDoorProfileList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + // +listType=set + Items []FrontDoorProfile `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FrontDoorProfile{}, &FrontDoorProfileList{}) +} diff --git a/api/v1alpha1/internalserviceexport_types.go b/api/v1alpha1/internalserviceexport_types.go index 21738e4a..c8ff75c7 100644 --- a/api/v1alpha1/internalserviceexport_types.go +++ b/api/v1alpha1/internalserviceexport_types.go @@ -36,6 +36,42 @@ type InternalServiceExportSpec struct { // If unspecified, weight defaults to 1. // The value is from serviceExport "networking.fleet.azure.com/weight" annotation and should be in the range [0, 1000]. Weight *int64 `json:"weight,omitempty"` + + // ExportMode selects which fleet-networking control plane on the hub + // consumes this export: + // - "L4-TrafficManager" (default): the TrafficManagerBackend + // reconciler picks it up and programs an Azure Traffic Manager + // endpoint (today's behaviour). + // - "L7-FrontDoor": the FrontDoorBackend reconciler (Phase 4) picks + // it up and programs an Azure Front Door origin backed by the + // Private Link Service referenced in PrivateLinkServiceResourceID. + // + // Sourced from the member ServiceExport's + // "networking.fleet.azure.com/export-mode" annotation (see + // objectmeta.ExtractExportModeFromServiceExport). The field is + // populated by the member serviceexport reconciler; hub controllers + // treat it as read-only. Absent value is equivalent to + // "L4-TrafficManager" so existing v1alpha1 objects (which pre-date + // this field) continue to route through ATM. + // + // +optional + // +kubebuilder:validation:Enum=L4-TrafficManager;L7-FrontDoor + ExportMode string `json:"exportMode,omitempty"` + + // PrivateLinkServiceResourceID is the Azure Resource URI of the + // Private Link Service (PLS) provisioned by cloud-provider-azure for + // the exported Service's internal load balancer. Populated only when + // ExportMode == "L7-FrontDoor" AND the Service carries the + // "service.beta.kubernetes.io/azure-pls-*" annotations that trigger + // PLS creation. The hub FrontDoorBackend reconciler wires this ID as + // an AFD private-link origin so global traffic can reach the ILB + // without traversing a public IP. + // + // Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/ + // Microsoft.Network/privateLinkServices/{name} + // + // +optional + PrivateLinkServiceResourceID *string `json:"privateLinkServiceResourceID,omitempty"` } // InternalServiceExportStatus contains the current status of an InternalServiceExport. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c5c9c1ed..dab9ec1c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -235,6 +235,459 @@ func (in *FromCluster) DeepCopy() *FromCluster { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorBackend) DeepCopyInto(out *FrontDoorBackend) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorBackend. +func (in *FrontDoorBackend) DeepCopy() *FrontDoorBackend { + if in == nil { + return nil + } + out := new(FrontDoorBackend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorBackend) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorBackendList) DeepCopyInto(out *FrontDoorBackendList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FrontDoorBackend, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorBackendList. +func (in *FrontDoorBackendList) DeepCopy() *FrontDoorBackendList { + if in == nil { + return nil + } + out := new(FrontDoorBackendList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorBackendList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorBackendRef) DeepCopyInto(out *FrontDoorBackendRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorBackendRef. +func (in *FrontDoorBackendRef) DeepCopy() *FrontDoorBackendRef { + if in == nil { + return nil + } + out := new(FrontDoorBackendRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorBackendSpec) DeepCopyInto(out *FrontDoorBackendSpec) { + *out = *in + out.Profile = in.Profile + out.Backend = in.Backend + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorBackendSpec. +func (in *FrontDoorBackendSpec) DeepCopy() *FrontDoorBackendSpec { + if in == nil { + return nil + } + out := new(FrontDoorBackendSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorBackendStatus) DeepCopyInto(out *FrontDoorBackendStatus) { + *out = *in + if in.Origins != nil { + in, out := &in.Origins, &out.Origins + *out = make([]FrontDoorOriginStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorBackendStatus. +func (in *FrontDoorBackendStatus) DeepCopy() *FrontDoorBackendStatus { + if in == nil { + return nil + } + out := new(FrontDoorBackendStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorCustomDomain) DeepCopyInto(out *FrontDoorCustomDomain) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorCustomDomain. +func (in *FrontDoorCustomDomain) DeepCopy() *FrontDoorCustomDomain { + if in == nil { + return nil + } + out := new(FrontDoorCustomDomain) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorCustomDomain) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorCustomDomainList) DeepCopyInto(out *FrontDoorCustomDomainList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FrontDoorCustomDomain, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorCustomDomainList. +func (in *FrontDoorCustomDomainList) DeepCopy() *FrontDoorCustomDomainList { + if in == nil { + return nil + } + out := new(FrontDoorCustomDomainList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorCustomDomainList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorCustomDomainSpec) DeepCopyInto(out *FrontDoorCustomDomainSpec) { + *out = *in + out.ProfileRef = in.ProfileRef + in.TLS.DeepCopyInto(&out.TLS) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorCustomDomainSpec. +func (in *FrontDoorCustomDomainSpec) DeepCopy() *FrontDoorCustomDomainSpec { + if in == nil { + return nil + } + out := new(FrontDoorCustomDomainSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorCustomDomainStatus) DeepCopyInto(out *FrontDoorCustomDomainStatus) { + *out = *in + if in.DNSValidationToken != nil { + in, out := &in.DNSValidationToken, &out.DNSValidationToken + *out = new(string) + **out = **in + } + if in.DNSValidationExpiry != nil { + in, out := &in.DNSValidationExpiry, &out.DNSValidationExpiry + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorCustomDomainStatus. +func (in *FrontDoorCustomDomainStatus) DeepCopy() *FrontDoorCustomDomainStatus { + if in == nil { + return nil + } + out := new(FrontDoorCustomDomainStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorKeyVaultCertificate) DeepCopyInto(out *FrontDoorKeyVaultCertificate) { + *out = *in + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorKeyVaultCertificate. +func (in *FrontDoorKeyVaultCertificate) DeepCopy() *FrontDoorKeyVaultCertificate { + if in == nil { + return nil + } + out := new(FrontDoorKeyVaultCertificate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorOriginStatus) DeepCopyInto(out *FrontDoorOriginStatus) { + *out = *in + if in.PrivateLinkServiceResourceID != nil { + in, out := &in.PrivateLinkServiceResourceID, &out.PrivateLinkServiceResourceID + *out = new(string) + **out = **in + } + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int64) + **out = **in + } + if in.From != nil { + in, out := &in.From, &out.From + *out = new(FromCluster) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorOriginStatus. +func (in *FrontDoorOriginStatus) DeepCopy() *FrontDoorOriginStatus { + if in == nil { + return nil + } + out := new(FrontDoorOriginStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfile) DeepCopyInto(out *FrontDoorProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfile. +func (in *FrontDoorProfile) DeepCopy() *FrontDoorProfile { + if in == nil { + return nil + } + out := new(FrontDoorProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfileList) DeepCopyInto(out *FrontDoorProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FrontDoorProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfileList. +func (in *FrontDoorProfileList) DeepCopy() *FrontDoorProfileList { + if in == nil { + return nil + } + out := new(FrontDoorProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FrontDoorProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfileRef) DeepCopyInto(out *FrontDoorProfileRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfileRef. +func (in *FrontDoorProfileRef) DeepCopy() *FrontDoorProfileRef { + if in == nil { + return nil + } + out := new(FrontDoorProfileRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfileReference) DeepCopyInto(out *FrontDoorProfileReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfileReference. +func (in *FrontDoorProfileReference) DeepCopy() *FrontDoorProfileReference { + if in == nil { + return nil + } + out := new(FrontDoorProfileReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfileSpec) DeepCopyInto(out *FrontDoorProfileSpec) { + *out = *in + if in.WAFPolicy != nil { + in, out := &in.WAFPolicy, &out.WAFPolicy + *out = new(FrontDoorWAFPolicyRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfileSpec. +func (in *FrontDoorProfileSpec) DeepCopy() *FrontDoorProfileSpec { + if in == nil { + return nil + } + out := new(FrontDoorProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorProfileStatus) DeepCopyInto(out *FrontDoorProfileStatus) { + *out = *in + if in.EndpointHostname != nil { + in, out := &in.EndpointHostname, &out.EndpointHostname + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorProfileStatus. +func (in *FrontDoorProfileStatus) DeepCopy() *FrontDoorProfileStatus { + if in == nil { + return nil + } + out := new(FrontDoorProfileStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorTLSConfig) DeepCopyInto(out *FrontDoorTLSConfig) { + *out = *in + if in.KeyVaultCertificate != nil { + in, out := &in.KeyVaultCertificate, &out.KeyVaultCertificate + *out = new(FrontDoorKeyVaultCertificate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorTLSConfig. +func (in *FrontDoorTLSConfig) DeepCopy() *FrontDoorTLSConfig { + if in == nil { + return nil + } + out := new(FrontDoorTLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontDoorWAFPolicyRef) DeepCopyInto(out *FrontDoorWAFPolicyRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontDoorWAFPolicyRef. +func (in *FrontDoorWAFPolicyRef) DeepCopy() *FrontDoorWAFPolicyRef { + if in == nil { + return nil + } + out := new(FrontDoorWAFPolicyRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalServiceExport) DeepCopyInto(out *InternalServiceExport) { *out = *in @@ -315,6 +768,11 @@ func (in *InternalServiceExportSpec) DeepCopyInto(out *InternalServiceExportSpec *out = new(int64) **out = **in } + if in.PrivateLinkServiceResourceID != nil { + in, out := &in.PrivateLinkServiceResourceID, &out.PrivateLinkServiceResourceID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalServiceExportSpec. diff --git a/config/crd/bases/networking.fleet.azure.com_frontdoorbackends.yaml b/config/crd/bases/networking.fleet.azure.com_frontdoorbackends.yaml new file mode 100644 index 00000000..48a7f69e --- /dev/null +++ b/config/crd/bases/networking.fleet.azure.com_frontdoorbackends.yaml @@ -0,0 +1,251 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: frontdoorbackends.networking.fleet.azure.com +spec: + group: networking.fleet.azure.com + names: + categories: + - fleet-networking + kind: FrontDoorBackend + listKind: FrontDoorBackendList + plural: frontdoorbackends + shortNames: + - fdb + singular: frontdoorbackend + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.profile.name + name: Profile + type: string + - jsonPath: .spec.backend.name + name: Backend + type: string + - jsonPath: .status.conditions[?(@.type=='Accepted')].status + name: Is-Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + FrontDoorBackend is the AFD-side counterpart to TrafficManagerBackend: it + binds a fleet-scoped ServiceImport (specifically, its L7-FrontDoor exports + carrying PrivateLinkServiceResourceID — see InternalServiceExportSpec) to a + FrontDoorProfile, producing an Azure Front Door OriginGroup with one Origin + per qualifying member-cluster export. + + The shape intentionally mirrors TrafficManagerBackend 1:1 so operators + authoring both surfaces have a single mental model: + + - Spec.Profile → parent FrontDoorProfile (same namespace) + - Spec.Backend → ServiceImport (same namespace) + - Spec.Weight → aggregate traffic weight, distributed across + per-export weights the same way as ATM (see the + TrafficManagerBackendSpec.Weight formula). + + Design references: + - docs/first-party/001-afd-global-load-balancing.md §4 (backend model) + - docs/first-party/002-afd-implementation-plan.md §6 (reconciler) + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: The desired state of FrontDoorBackend. + properties: + backend: + description: The reference to a backend. + properties: + name: + description: |- + Name is the reference to the ServiceImport in the same namespace as + the FrontDoorBackend object. + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: spec.backend is immutable + rule: self == oldSelf + profile: + description: Which FrontDoorProfile the backend should be attached + to. + properties: + name: + description: Name is the name of the referenced FrontDoorProfile. + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: spec.profile is immutable + rule: self == oldSelf + weight: + default: 1 + description: |- + The aggregate weight of origins behind the serviceImport under the + generated Front Door OriginGroup. Possible values are from 0 to 1000. + Semantics match TrafficManagerBackendSpec.Weight: the value is + distributed across each cluster's exports proportionally to the + per-export weight surfaced on the ServiceExport. If weight is set to + 0, all origins behind the serviceImport will be removed from the + OriginGroup (effectively draining traffic). + format: int64 + maximum: 1000 + minimum: 0 + type: integer + required: + - backend + - profile + type: object + status: + description: The observed status of FrontDoorBackend. + properties: + conditions: + description: Current backend status. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + originGroupResourceID: + description: |- + OriginGroupResourceID is the fully qualified Azure resource Id of + the Origin Group created for this backend. Empty while the + backend is still being accepted. + type: string + origins: + description: |- + Origins contains a list of accepted Azure Front Door origins that + are created or updated under the generated OriginGroup. + items: + description: |- + FrontDoorOriginStatus captures the status of a single Azure Front Door + Origin created under the FrontDoorProfile for this backend. + properties: + from: + description: From is where the origin's underlying export was + sourced. + properties: + cluster: + description: cluster is the name of the exporting cluster. + Must be a valid RFC-1123 DNS label. + type: string + weight: + description: |- + Weight defines the weight configured in the serviceExport from the source cluster. + Possible values are from 0 to 1000. + format: int64 + type: integer + required: + - cluster + type: object + name: + description: Name of the origin (as created inside the AFD OriginGroup). + type: string + privateLinkServiceResourceID: + description: |- + PrivateLinkServiceResourceID is the PLS this origin was wired to + (see InternalServiceExportSpec.PrivateLinkServiceResourceID). + type: string + resourceID: + description: |- + ResourceID is the fully qualified Azure resource Id of the origin. + Ex - /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{profile}/originGroups/{group}/origins/{name} + type: string + weight: + description: |- + Weight is the effective weight of this origin, after the aggregate + FrontDoorBackendSpec.Weight has been distributed across per-export + weights. Semantics parallel TrafficManagerEndpointStatus.Weight. + format: int64 + type: integer + required: + - name + type: object + type: array + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: metadata.name max length is 63 + rule: size(self.metadata.name) < 64 + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/networking.fleet.azure.com_frontdoorcustomdomains.yaml b/config/crd/bases/networking.fleet.azure.com_frontdoorcustomdomains.yaml new file mode 100644 index 00000000..87cafc65 --- /dev/null +++ b/config/crd/bases/networking.fleet.azure.com_frontdoorcustomdomains.yaml @@ -0,0 +1,235 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: frontdoorcustomdomains.networking.fleet.azure.com +spec: + group: networking.fleet.azure.com + names: + categories: + - fleet-networking + kind: FrontDoorCustomDomain + listKind: FrontDoorCustomDomainList + plural: frontdoorcustomdomains + shortNames: + - afdcd + singular: frontdoorcustomdomain + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostname + name: Hostname + type: string + - jsonPath: .status.validationState + name: Validation + type: string + - jsonPath: .status.conditions[?(@.type=='Programmed')].status + name: Is-Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + FrontDoorCustomDomain represents a custom domain attached to a FrontDoorProfile, + including the DNS-based ownership validation and the TLS binding. + https://learn.microsoft.com/en-us/azure/frontdoor/domain + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: The desired state of FrontDoorCustomDomain. + properties: + hostname: + description: |- + Hostname is the fully qualified custom domain name (e.g. www.contoso.com). + Immutable after creation. + maxLength: 253 + minLength: 1 + pattern: ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ + type: string + x-kubernetes-validations: + - message: hostname is immutable + rule: self == oldSelf + profileRef: + description: |- + ProfileRef references the FrontDoorProfile that owns this custom domain. + The referenced profile must exist in the same namespace as this resource. + Immutable after creation. + properties: + name: + description: |- + Name of the target FrontDoorProfile. Must exist in the same namespace as + this FrontDoorCustomDomain (per breadcrumb D4: same-namespace only). + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: profileRef is immutable + rule: self == oldSelf + tls: + description: TLS configures the certificate binding for this custom + domain. + properties: + keyVaultCertificate: + description: KeyVaultCertificate references the certificate to + bind when Mode is BYOC. + properties: + certificateName: + description: CertificateName is the name of the certificate + in the Key Vault. + maxLength: 127 + minLength: 1 + type: string + vaultURI: + description: VaultURI is the base URI of the Key Vault, e.g. + https://myvault.vault.azure.net. + minLength: 1 + pattern: ^https://[a-zA-Z0-9-]+\.vault\.azure\.net/?$ + type: string + version: + description: |- + Version optionally pins a specific certificate version. When omitted, + the controller tracks the latest version and re-binds on rotation. + type: string + required: + - certificateName + - vaultURI + type: object + mode: + description: Mode selects the source of the TLS certificate. + enum: + - Managed + - BYOC + type: string + required: + - mode + type: object + x-kubernetes-validations: + - message: keyVaultCertificate is required when mode is BYOC + rule: self.mode != 'BYOC' || has(self.keyVaultCertificate) + - message: keyVaultCertificate must not be set when mode is Managed + rule: self.mode != 'Managed' || !has(self.keyVaultCertificate) + required: + - hostname + - profileRef + - tls + type: object + status: + description: The observed status of FrontDoorCustomDomain. + properties: + conditions: + description: Current custom domain status. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + dnsValidationExpiry: + description: |- + DNSValidationExpiry is the time at which the current validation token + expires. Azure issues a new token periodically; the controller refreshes + status before expiry. + format: date-time + type: string + dnsValidationToken: + description: |- + DNSValidationToken is the token that must be published as a TXT record on + the customer's DNS zone to prove ownership. The expected TXT record name + is `_dnsauth.`, and the value is this token. + type: string + resourceID: + description: |- + ResourceID is the fully qualified Azure resource ID of the custom domain. + Example: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{profile}/customDomains/{name} + type: string + validationState: + description: ValidationState is the current DNS ownership validation + state reported by Azure. + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: metadata.name max length is 63 + rule: size(self.metadata.name) < 64 + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/networking.fleet.azure.com_frontdoorprofiles.yaml b/config/crd/bases/networking.fleet.azure.com_frontdoorprofiles.yaml new file mode 100644 index 00000000..809c3ec9 --- /dev/null +++ b/config/crd/bases/networking.fleet.azure.com_frontdoorprofiles.yaml @@ -0,0 +1,241 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: frontdoorprofiles.networking.fleet.azure.com +spec: + group: networking.fleet.azure.com + names: + categories: + - fleet-networking + kind: FrontDoorProfile + listKind: FrontDoorProfileList + plural: frontdoorprofiles + shortNames: + - afdp + singular: frontdoorprofile + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.endpointHostname + name: Endpoint + type: string + - jsonPath: .status.conditions[?(@.type=='Programmed')].status + name: Is-Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + FrontDoorProfile manages an Azure Front Door profile and its default endpoint + using the cloud-native (Kubernetes) API. It is the L7 counterpart to + TrafficManagerProfile. + https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: The desired state of FrontDoorProfile. + properties: + complianceMode: + default: None + description: |- + ComplianceMode declares the security/compliance regime this profile + (and its backends) must satisfy. See docs/first-party/001-afd-global-load-balancing.md + §2.1 for the design and docs/first-party/003-pre-implementation-checklist.md §1.2 + for the operational requirements. + + Effects of the value: + - "None" (default): no additional constraints beyond the structural + ones. Suitable for dev/test tenants that don't need SFI-NS253 + compliance, and lets the profile be created without a WAF policy + attach (WAFPolicy stays optional). + - "SFI-NS253": + * spec.wafPolicy is REQUIRED (enforced by the cross-field CEL + rule on this Spec — see below). + * The FrontDoorProfile reconciler additionally verifies that the + referenced WAF policy exists and is in Prevention mode + (surfaced as Programmed=False with + Reason=WAFPolicyNotFound / WAFPolicyNotInPreventionMode). + * Every FrontDoorBackend that references this profile must have + spec.privateLink.enabled = true (enforced in the backend + reconciler, surfaced as Accepted=False, + Reason=SFIComplianceViolation). + + Immutable after creation: switching a live profile out of SFI-NS253 + would silently weaken guarantees the operator relied on when + creating the profile — recreate the profile if the compliance regime + legitimately needs to change. + enum: + - None + - SFI-NS253 + type: string + x-kubernetes-validations: + - message: complianceMode is immutable + rule: self == oldSelf + resourceGroup: + description: |- + ResourceGroup is the name of the Azure resource group in which the + underlying Front Door profile will be created. Immutable after creation. + maxLength: 90 + minLength: 1 + type: string + x-kubernetes-validations: + - message: resourceGroup is immutable + rule: self == oldSelf + sku: + default: Premium_AzureFrontDoor + description: |- + Sku selects the Front Door SKU. Premium is the only supported value + (see the FrontDoorProfileSkuName type comment for the SFI-NS253 + rationale). Retained as an explicit field — even though the enum is + currently single-valued — so future SKUs (if AFD ever ships a + compliance-equivalent alternative) can be introduced additively + without a schema break. Immutable after creation because AFD does + not support in-place SKU upgrades on an existing profile. + enum: + - Premium_AzureFrontDoor + type: string + x-kubernetes-validations: + - message: sku is immutable + rule: self == oldSelf + wafPolicy: + description: |- + WAFPolicy attaches a Web Application Firewall policy to the profile. + + Required when ComplianceMode is "SFI-NS253"; optional otherwise (dev/test + tenants may run without a WAF attach). The cross-field CEL rule below + rejects any object that violates this at admission time so that a + misconfigured SFI-NS253 profile never even reaches the reconciler. + properties: + resourceID: + description: |- + ResourceID is the fully qualified ARM resource ID of an existing + Microsoft.Network/frontdoorwebapplicationfirewallpolicies resource. + + Format: + /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/frontdoorwebapplicationfirewallpolicies/{name} + + The policy may live in a different subscription/RG from the AFD + profile; cross-subscription references are supported at the Azure + level, subject to the AFD controller's managed identity having + Microsoft.Network/frontDoorWebApplicationFirewallPolicies/read on + the policy's scope. Cross-sub RBAC failures surface as + Programmed=False, Reason=WAFPolicyNotFound with the exact ID in the + message (per docs/first-party/002-afd-implementation-plan.md §11). + minLength: 1 + pattern: ^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\.Network/frontdoorwebapplicationfirewallpolicies/[^/]+$ + type: string + required: + - resourceID + type: object + required: + - resourceGroup + - sku + type: object + status: + description: The observed status of FrontDoorProfile. + properties: + conditions: + description: Current profile status. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + endpointHostname: + description: |- + EndpointHostname is the default *.azurefd.net hostname assigned by Azure + to the profile's default endpoint. Populated once the endpoint is programmed. + type: string + resourceID: + description: |- + ResourceID is the fully qualified Azure resource ID of the Front Door profile. + Example: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Cdn/profiles/{name} + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: metadata.name max length is 63 + rule: size(self.metadata.name) < 64 + - message: spec.wafPolicy is required when spec.complianceMode is SFI-NS253 + rule: self.spec.complianceMode != 'SFI-NS253' || has(self.spec.wafPolicy) + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml b/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml index 33558d5a..aab2808c 100644 --- a/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml +++ b/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml @@ -47,6 +47,28 @@ spec: InternalServiceExportSpec specifies the spec of an exported Service; at this stage only the ports of an exported Service are sync'd. properties: + exportMode: + description: |- + ExportMode selects which fleet-networking control plane on the hub + consumes this export: + - "L4-TrafficManager" (default): the TrafficManagerBackend + reconciler picks it up and programs an Azure Traffic Manager + endpoint (today's behaviour). + - "L7-FrontDoor": the FrontDoorBackend reconciler (Phase 4) picks + it up and programs an Azure Front Door origin backed by the + Private Link Service referenced in PrivateLinkServiceResourceID. + + Sourced from the member ServiceExport's + "networking.fleet.azure.com/export-mode" annotation (see + objectmeta.ExtractExportModeFromServiceExport). The field is + populated by the member serviceexport reconciler; hub controllers + treat it as read-only. Absent value is equivalent to + "L4-TrafficManager" so existing v1alpha1 objects (which pre-date + this field) continue to route through ATM. + enum: + - L4-TrafficManager + - L7-FrontDoor + type: string isDNSLabelConfigured: description: |- IsDNSLabelConfigured determines if the Service has a DNS label configured. @@ -111,6 +133,20 @@ spec: type: object type: array x-kubernetes-list-type: atomic + privateLinkServiceResourceID: + description: |- + PrivateLinkServiceResourceID is the Azure Resource URI of the + Private Link Service (PLS) provisioned by cloud-provider-azure for + the exported Service's internal load balancer. Populated only when + ExportMode == "L7-FrontDoor" AND the Service carries the + "service.beta.kubernetes.io/azure-pls-*" annotations that trigger + PLS creation. The hub FrontDoorBackend reconciler wires this ID as + an AFD private-link origin so global traffic can reach the ILB + without traversing a public IP. + + Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/ + Microsoft.Network/privateLinkServices/{name} + type: string publicIPResourceID: description: PublicIPResourceID is the Azure Resource URI of public IP. This is only applicable for Load Balancer type Services.