From 94b944483c3053f6ac964fb01e040008c8510098 Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Mon, 6 Apr 2026 16:36:52 +0200 Subject: [PATCH 1/4] fix(deps): update hypershift API to latest main Update github.com/openshift/hypershift/api to v0.0.0-20260406110001-bcf6adaf131f. This brings in the HCPEtcdBackup CRD types, HCPEtcdBackupConfig in ManagedEtcdSpec.Backup, and related condition/reason constants needed for CNTRLPLANE-2685 integration. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Juan Manuel Parrilla Madrid --- go.mod | 4 +- go.sum | 12 +- .../api/config/v1/types_apiserver.go | 1 + .../api/config/v1/types_authentication.go | 315 +++++----- .../api/config/v1/types_infrastructure.go | 7 +- .../openshift/api/config/v1/types_insights.go | 1 + .../openshift/api/config/v1/types_network.go | 4 +- .../api/config/v1/types_tlssecurityprofile.go | 49 +- ..._generated.featuregated-crd-manifests.yaml | 6 +- .../v1/zz_generated.swagger_doc_generated.go | 44 +- .../api/operator/v1/types_network.go | 6 +- ..._generated.featuregated-crd-manifests.yaml | 5 +- .../api/hypershift/v1beta1/azure.go | 113 ++++ .../v1beta1/azureprivatelinkservice_types.go | 309 ++++++++++ .../v1beta1/controlplaneversion_types.go | 70 +++ .../hypershift/v1beta1/etcdbackup_types.go | 360 ++++++++++++ .../hypershift/api/hypershift/v1beta1/gcp.go | 142 +++-- .../v1beta1/gcpprivateserviceconnect_types.go | 44 +- .../hypershift/v1beta1/hosted_controlplane.go | 11 + .../v1beta1/hostedcluster_conditions.go | 15 + .../hypershift/v1beta1/hostedcluster_types.go | 73 ++- .../api/hypershift/v1beta1/nodepool_types.go | 53 ++ .../api/hypershift/v1beta1/operator.go | 18 + .../v1beta1/zz_generated.deepcopy.go | 553 ++++++++++++++++-- ..._generated.featuregated-crd-manifests.yaml | 90 ++- vendor/modules.txt | 4 +- 26 files changed, 1916 insertions(+), 393 deletions(-) create mode 100644 vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azureprivatelinkservice_types.go create mode 100644 vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/controlplaneversion_types.go create mode 100644 vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/etcdbackup_types.go diff --git a/go.mod b/go.mod index 503e93c38..56fc4cec5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0 github.com/onsi/gomega v1.39.1 github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff - github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0 + github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9 github.com/sirupsen/logrus v1.9.4 github.com/vmware-tanzu/velero v1.14.0 k8s.io/api v0.35.3 @@ -26,7 +26,7 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/openshift/api v0.0.0-20260120150926-4c643a652d54 // indirect + github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f // indirect github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect diff --git a/go.sum b/go.sum index c8c5317fe..fbec6d3ea 100644 --- a/go.sum +++ b/go.sum @@ -192,8 +192,8 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= -github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= @@ -201,14 +201,14 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/openshift/api v0.0.0-20260120150926-4c643a652d54 h1:Gm81lfkiXFgg/N0x90WsplERGF2NqYjK0vd8YY/aFpU= -github.com/openshift/api v0.0.0-20260120150926-4c643a652d54/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f h1:M8y0oBq/KRkuSNFlUMQRAn2MrXJh1mzTCFgbLpPWQbM= +github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 h1:cHyxR+Y8rAMT6m1jQCaYGRwikqahI0OjjUDhFNf3ySQ= github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff h1:6C1z4xMAruyeiTFGqahxNDpI1cXPCjpaFeIeIodty08= github.com/openshift/hive/apis v0.0.0-20241220022629-3f49f26197ff/go.mod h1:1vBNCcWNpQyFCz83PWYT/lHUFJ9ost2t5FijHElh6gQ= -github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0 h1:x5DgHyFXF9zpgTH4JYDpBhQnASEIj6z1WXdeHl1DGAI= -github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0/go.mod h1:eYDwJzXCU+0HO9DdvhBAA143z7woIJ5dV71TaJXIkgk= +github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9 h1:mVzLud8ewZi1W8dnV37L+eY9IJsoFfpkASgPvDRM61Q= +github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9/go.mod h1:mC9+bqb81FmG924VOO+7P0ofKQgvY1SdPhFp0oJ9U44= github.com/openshift/velero v0.10.2-0.20260323170432-5ef912f438f6 h1:nWv9+tEi34VMFMOziVZLOKvn3yXDqk68ODXlzpYU2S0= github.com/openshift/velero v0.10.2-0.20260323170432-5ef912f438f6/go.mod h1:YKHPM2FpS+5WrcciMi5j4bo/JMnXXrR1M+j1DoTA26U= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/vendor/github.com/openshift/api/config/v1/types_apiserver.go b/vendor/github.com/openshift/api/config/v1/types_apiserver.go index 0afe7b1d8..31d888185 100644 --- a/vendor/github.com/openshift/api/config/v1/types_apiserver.go +++ b/vendor/github.com/openshift/api/config/v1/types_apiserver.go @@ -212,6 +212,7 @@ type APIServerEncryption struct { // +openshift:validation:FeatureGateAwareEnum:featureGate="",enum="";identity;aescbc;aesgcm // +openshift:validation:FeatureGateAwareEnum:featureGate=KMSEncryptionProvider,enum="";identity;aescbc;aesgcm;KMS +// +openshift:validation:FeatureGateAwareEnum:featureGate=KMSEncryption,enum="";identity;aescbc;aesgcm;KMS type EncryptionType string const ( diff --git a/vendor/github.com/openshift/api/config/v1/types_authentication.go b/vendor/github.com/openshift/api/config/v1/types_authentication.go index e300d4eab..64d0f399b 100644 --- a/vendor/github.com/openshift/api/config/v1/types_authentication.go +++ b/vendor/github.com/openshift/api/config/v1/types_authentication.go @@ -80,8 +80,7 @@ type AuthenticationSpec struct { // +optional ServiceAccountIssuer string `json:"serviceAccountIssuer"` - // oidcProviders are OIDC identity providers that can issue tokens - // for this cluster + // oidcProviders are OIDC identity providers that can issue tokens for this cluster // Can only be set if "Type" is set to "OIDC". // // At most one provider can be configured. @@ -113,8 +112,7 @@ type AuthenticationStatus struct { // +optional IntegratedOAuthMetadata ConfigMapNameReference `json:"integratedOAuthMetadata"` - // oidcClients is where participating operators place the current OIDC client status - // for OIDC clients that can be customized by the cluster-admin. + // oidcClients is where participating operators place the current OIDC client status for OIDC clients that can be customized by the cluster-admin. // // +listType=map // +listMapKey=componentNamespace @@ -146,8 +144,7 @@ type AuthenticationType string const ( // None means that no cluster managed authentication system is in place. - // Note that user login will only work if a manually configured system is in place and - // referenced in authentication spec via oauthMetadata and + // Note that user login will only work if a manually configured system is in place and referenced in authentication spec via oauthMetadata and // webhookTokenAuthenticator/oidcProviders AuthenticationTypeNone AuthenticationType = "None" @@ -199,10 +196,8 @@ const ( ) type OIDCProvider struct { - // name is a required field that configures the unique human-readable identifier - // associated with the identity provider. - // It is used to distinguish between multiple identity providers - // and has no impact on token validation or authentication mechanics. + // name is a required field that configures the unique human-readable identifier associated with the identity provider. + // It is used to distinguish between multiple identity providers and has no impact on token validation or authentication mechanics. // // name must not be an empty string (""). // @@ -210,15 +205,12 @@ type OIDCProvider struct { // +required Name string `json:"name"` - // issuer is a required field that configures how the platform interacts - // with the identity provider and how tokens issued from the identity provider - // are evaluated by the Kubernetes API server. + // issuer is a required field that configures how the platform interacts with the identity provider and how tokens issued from the identity provider are evaluated by the Kubernetes API server. // // +required Issuer TokenIssuer `json:"issuer"` - // oidcClients is an optional field that configures how on-cluster, - // platform clients should request tokens from the identity provider. + // oidcClients is an optional field that configures how on-cluster, platform clients should request tokens from the identity provider. // oidcClients must not exceed 20 entries and entries must have unique namespace/name pairs. // // +listType=map @@ -228,16 +220,12 @@ type OIDCProvider struct { // +optional OIDCClients []OIDCClientConfig `json:"oidcClients"` - // claimMappings is a required field that configures the rules to be used by - // the Kubernetes API server for translating claims in a JWT token, issued - // by the identity provider, to a cluster identity. + // claimMappings is a required field that configures the rules to be used by the Kubernetes API server for translating claims in a JWT token, issued by the identity provider, to a cluster identity. // // +required ClaimMappings TokenClaimMappings `json:"claimMappings"` - // claimValidationRules is an optional field that configures the rules to - // be used by the Kubernetes API server for validating the claims in a JWT - // token issued by the identity provider. + // claimValidationRules is an optional field that configures the rules to be used by the Kubernetes API server for validating the claims in a JWT token issued by the identity provider. // // Validation rules are joined via an AND operation. // @@ -245,9 +233,7 @@ type OIDCProvider struct { // +optional ClaimValidationRules []TokenClaimValidationRule `json:"claimValidationRules,omitempty"` - // userValidationRules is an optional field that configures the set of rules - // used to validate the cluster user identity that was constructed via - // mapping token claims to user identity attributes. + // userValidationRules is an optional field that configures the set of rules used to validate the cluster user identity that was constructed via mapping token claims to user identity attributes. // Rules are CEL expressions that must evaluate to 'true' for authentication to succeed. // If any rule in the chain of rules evaluates to 'false', authentication will fail. // When specified, at least one rule must be specified and no more than 64 rules may be specified. @@ -266,10 +252,8 @@ type TokenAudience string // +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUpstreamParity,rule="self.?discoveryURL.orValue(\"\").size() > 0 ? (self.issuerURL.size() == 0 || self.discoveryURL.find('^.+[^/]') != self.issuerURL.find('^.+[^/]')) : true",message="discoveryURL must be different from issuerURL" type TokenIssuer struct { - // issuerURL is a required field that configures the URL used to issue tokens - // by the identity provider. - // The Kubernetes API server determines how authentication tokens should be handled - // by matching the 'iss' claim in the JWT to the issuerURL of configured identity providers. + // issuerURL is a required field that configures the URL used to issue tokens by the identity provider. + // The Kubernetes API server determines how authentication tokens should be handled by matching the 'iss' claim in the JWT to the issuerURL of configured identity providers. // // Must be at least 1 character and must not exceed 512 characters in length. // Must be a valid URL that uses the 'https' scheme and does not contain a query, fragment or user. @@ -284,8 +268,7 @@ type TokenIssuer struct { // +required URL string `json:"issuerURL"` - // audiences is a required field that configures the acceptable audiences - // the JWT token, issued by the identity provider, must be issued to. + // audiences is a required field that configures the acceptable audiences the JWT token, issued by the identity provider, must be issued to. // At least one of the entries must match the 'aud' claim in the JWT token. // // audiences must contain at least one entry and must not exceed ten entries. @@ -296,24 +279,20 @@ type TokenIssuer struct { // +required Audiences []TokenAudience `json:"audiences"` - // issuerCertificateAuthority is an optional field that configures the - // certificate authority, used by the Kubernetes API server, to validate - // the connection to the identity provider when fetching discovery information. + // issuerCertificateAuthority is an optional field that configures the certificate authority, used by the Kubernetes API server, to validate the connection to the identity provider when fetching discovery information. // // When not specified, the system trust is used. // - // When specified, it must reference a ConfigMap in the openshift-config - // namespace containing the PEM-encoded CA certificates under the 'ca-bundle.crt' - // key in the data field of the ConfigMap. + // When specified, it must reference a ConfigMap in the openshift-config namespace containing the PEM-encoded CA certificates under the 'ca-bundle.crt' key in the data field of the ConfigMap. // // +optional CertificateAuthority ConfigMapNameReference `json:"issuerCertificateAuthority"` - // discoveryURL is an optional field that, if specified, overrides the default discovery endpoint - // used to retrieve OIDC configuration metadata. By default, the discovery URL is derived from `issuerURL` - // as "{issuerURL}/.well-known/openid-configuration". + // discoveryURL is an optional field that, if specified, overrides the default discovery endpoint used to retrieve OIDC configuration metadata. + // By default, the discovery URL is derived from `issuerURL` as "{issuerURL}/.well-known/openid-configuration". // - // The discoveryURL must be a valid absolute HTTPS URL. It must not contain query - // parameters, user information, or fragments. Additionally, it must differ from the value of `url` (ignoring trailing slashes). + // The discoveryURL must be a valid absolute HTTPS URL. + // It must not contain query parameters, user information, or fragments. + // Additionally, it must differ from the value of `issuerURL` (ignoring trailing slashes). // The discoveryURL value must be at least 1 character long and no longer than 2048 characters. // // +optional @@ -329,39 +308,36 @@ type TokenIssuer struct { } type TokenClaimMappings struct { - // username is a required field that configures how the username of a cluster identity - // should be constructed from the claims in a JWT token issued by the identity provider. + // username is a required field that configures how the username of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider. // // +required Username UsernameClaimMapping `json:"username"` - // groups is an optional field that configures how the groups of a cluster identity - // should be constructed from the claims in a JWT token issued - // by the identity provider. - // When referencing a claim, if the claim is present in the JWT - // token, its value must be a list of groups separated by a comma (','). + // groups is an optional field that configures how the groups of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider. + // + // When referencing a claim, if the claim is present in the JWT token, its value must be a list of groups separated by a comma (','). + // // For example - '"example"' and '"exampleOne", "exampleTwo", "exampleThree"' are valid claim values. // // +optional Groups PrefixedClaimMapping `json:"groups,omitempty"` - // uid is an optional field for configuring the claim mapping - // used to construct the uid for the cluster identity. + // uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity. // // When using uid.claim to specify the claim it must be a single string value. // When using uid.expression the expression must result in a single string value. // - // When omitted, this means the user has no opinion and the platform - // is left to choose a default, which is subject to change over time. + // When omitted, this means the user has no opinion and the platform is left to choose a default, which is subject to change over time. + // // The current default is to use the 'sub' claim. // // +optional // +openshift:enable:FeatureGate=ExternalOIDCWithUIDAndExtraClaimMappings UID *TokenClaimOrExpressionMapping `json:"uid,omitempty"` - // extra is an optional field for configuring the mappings - // used to construct the extra attribute for the cluster identity. + // extra is an optional field for configuring the mappings used to construct the extra attribute for the cluster identity. // When omitted, no extra attributes will be present on the cluster identity. + // // key values for extra mappings must be unique. // A maximum of 32 extra attribute mappings may be provided. // @@ -373,52 +349,63 @@ type TokenClaimMappings struct { Extra []ExtraMapping `json:"extra,omitempty"` } -// TokenClaimMapping allows specifying a JWT token -// claim to be used when mapping claims from an -// authentication token to cluster identities. +// TokenClaimMapping allows specifying a JWT token claim to be used when mapping claims from an authentication token to cluster identities. +// +openshift:validation:FeatureGateAwareXValidation:featureGate="",rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC,rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUIDAndExtraClaimMappings,rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUpstreamParity,rule="(size(self.?claim.orValue(\"\")) > 0) ? !has(self.expression) : true",message="expression must not be set if claim is specified and is not an empty string" type TokenClaimMapping struct { - // claim is a required field that configures the JWT token - // claim whose value is assigned to the cluster identity - // field associated with this mapping. + // claim is an optional field for specifying the JWT token claim that is used in the mapping. + // The value of this claim will be assigned to the field in which this mapping is associated. + // claim must not exceed 256 characters in length. + // When set to the empty string `""`, this means that no named claim should be used for the group mapping. + // claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. // - // +required + // +optional + // +kubebuilder:validation:MaxLength=256 Claim string `json:"claim"` + + // expression is an optional CEL expression used to derive + // group values from JWT claims. + // + // CEL expressions have access to the token claims through a CEL variable, 'claims'. + // + // expression must be at least 1 character and must not exceed 1024 characters in length . + // + // When specified, claim must not be set or be explicitly set to the empty string (`""`). + // + // +optional + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Expression string `json:"expression,omitempty"` } -// TokenClaimOrExpressionMapping allows specifying either a JWT -// token claim or CEL expression to be used when mapping claims -// from an authentication token to cluster identities. +// TokenClaimOrExpressionMapping allows specifying either a JWT token claim or CEL expression to be used when mapping claims from an authentication token to cluster identities. // +kubebuilder:validation:XValidation:rule="has(self.claim) ? !has(self.expression) : has(self.expression)",message="precisely one of claim or expression must be set" type TokenClaimOrExpressionMapping struct { - // claim is an optional field for specifying the - // JWT token claim that is used in the mapping. - // The value of this claim will be assigned to - // the field in which this mapping is associated. + // claim is an optional field for specifying the JWT token claim that is used in the mapping. + // The value of this claim will be assigned to the field in which this mapping is associated. // // Precisely one of claim or expression must be set. // claim must not be specified when expression is set. - // When specified, claim must be at least 1 character in length - // and must not exceed 256 characters in length. + // When specified, claim must be at least 1 character in length and must not exceed 256 characters in length. // // +optional // +kubebuilder:validation:MaxLength=256 // +kubebuilder:validation:MinLength=1 Claim string `json:"claim,omitempty"` - // expression is an optional field for specifying a - // CEL expression that produces a string value from - // JWT token claims. + // expression is an optional field for specifying a CEL expression that produces a string value from JWT token claims. // - // CEL expressions have access to the token claims - // through a CEL variable, 'claims'. + // CEL expressions have access to the token claims through a CEL variable, 'claims'. // 'claims' is a map of claim names to claim values. // For example, the 'sub' claim value can be accessed as 'claims.sub'. // Nested claims can be accessed using dot notation ('claims.foo.bar'). // // Precisely one of claim or expression must be set. // expression must not be specified when claim is set. - // When specified, expression must be at least 1 character in length - // and must not exceed 1024 characters in length. + // When specified, expression must be at least 1 character in length and must not exceed 1024 characters in length. // // +optional // +kubebuilder:validation:MaxLength=1024 @@ -426,13 +413,10 @@ type TokenClaimOrExpressionMapping struct { Expression string `json:"expression,omitempty"` } -// ExtraMapping allows specifying a key and CEL expression -// to evaluate the keys' value. It is used to create additional -// mappings and attributes added to a cluster identity from -// a provided authentication token. +// ExtraMapping allows specifying a key and CEL expression to evaluate the keys' value. +// It is used to create additional mappings and attributes added to a cluster identity from a provided authentication token. type ExtraMapping struct { - // key is a required field that specifies the string - // to use as the extra attribute key. + // key is a required field that specifies the string to use as the extra attribute key. // // key must be a domain-prefix path (e.g 'example.org/foo'). // key must not exceed 510 characters in length. @@ -445,8 +429,7 @@ type ExtraMapping struct { // It must only contain lower case alphanumeric characters and '-' or '.'. // It must not use the reserved domains, or be subdomains of, "kubernetes.io", "k8s.io", and "openshift.io". // - // The path portion of the key (string of characters after the '/') must not be empty and must consist of at least one - // alphanumeric character, percent-encoded octets, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '=', and ':'. + // The path portion of the key (string of characters after the '/') must not be empty and must consist of at least one alphanumeric character, percent-encoded octets, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '=', and ':'. // It must not exceed 256 characters in length. // // +required @@ -468,14 +451,12 @@ type ExtraMapping struct { // +kubebuilder:validation:XValidation:rule="self.split('/', 2)[1].size() <= 256",message="the path of the key must not exceed 256 characters in length" Key string `json:"key"` - // valueExpression is a required field to specify the CEL expression to extract - // the extra attribute value from a JWT token's claims. + // valueExpression is a required field to specify the CEL expression to extract the extra attribute value from a JWT token's claims. // valueExpression must produce a string or string array value. // "", [], and null are treated as the extra mapping not being present. // Empty string values within an array are filtered out. // - // CEL expressions have access to the token claims - // through a CEL variable, 'claims'. + // CEL expressions have access to the token claims through a CEL variable, 'claims'. // 'claims' is a map of claim names to claim values. // For example, the 'sub' claim value can be accessed as 'claims.sub'. // Nested claims can be accessed using dot notation ('claims.foo.bar'). @@ -489,12 +470,10 @@ type ExtraMapping struct { ValueExpression string `json:"valueExpression"` } -// OIDCClientConfig configures how platform clients -// interact with identity providers as an authentication -// method +// OIDCClientConfig configures how platform clients interact with identity providers as an authentication method. type OIDCClientConfig struct { - // componentName is a required field that specifies the name of the platform - // component being configured to use the identity provider as an authentication mode. + // componentName is a required field that specifies the name of the platform component being configured to use the identity provider as an authentication mode. + // // It is used in combination with componentNamespace as a unique identifier. // // componentName must not be an empty string ("") and must not exceed 256 characters in length. @@ -504,9 +483,8 @@ type OIDCClientConfig struct { // +required ComponentName string `json:"componentName"` - // componentNamespace is a required field that specifies the namespace in which the - // platform component being configured to use the identity provider as an authentication - // mode is running. + // componentNamespace is a required field that specifies the namespace in which the platform component being configured to use the identity provider as an authentication mode is running. + // // It is used in combination with componentName as a unique identifier. // // componentNamespace must not be an empty string ("") and must not exceed 63 characters in length. @@ -516,11 +494,8 @@ type OIDCClientConfig struct { // +required ComponentNamespace string `json:"componentNamespace"` - // clientID is a required field that configures the client identifier, from - // the identity provider, that the platform component uses for authentication - // requests made to the identity provider. - // The identity provider must accept this identifier for platform components - // to be able to use the identity provider as an authentication mode. + // clientID is a required field that configures the client identifier, from the identity provider, that the platform component uses for authentication requests made to the identity provider. + // The identity provider must accept this identifier for platform components to be able to use the identity provider as an authentication mode. // // clientID must not be an empty string (""). // @@ -528,27 +503,21 @@ type OIDCClientConfig struct { // +required ClientID string `json:"clientID"` - // clientSecret is an optional field that configures the client secret used - // by the platform component when making authentication requests to the identity provider. + // clientSecret is an optional field that configures the client secret used by the platform component when making authentication requests to the identity provider. // - // When not specified, no client secret will be used when making authentication requests - // to the identity provider. + // When not specified, no client secret will be used when making authentication requests to the identity provider. + // + // When specified, clientSecret references a Secret in the 'openshift-config' namespace that contains the client secret in the 'clientSecret' key of the '.data' field. // - // When specified, clientSecret references a Secret in the 'openshift-config' - // namespace that contains the client secret in the 'clientSecret' key of the '.data' field. // The client secret will be used when making authentication requests to the identity provider. // - // Public clients do not require a client secret but private - // clients do require a client secret to work with the identity provider. + // Public clients do not require a client secret but private clients do require a client secret to work with the identity provider. // // +optional ClientSecret SecretNameReference `json:"clientSecret"` - // extraScopes is an optional field that configures the extra scopes that should - // be requested by the platform component when making authentication requests to the - // identity provider. - // This is useful if you have configured claim mappings that requires specific - // scopes to be requested beyond the standard OIDC scopes. + // extraScopes is an optional field that configures the extra scopes that should be requested by the platform component when making authentication requests to the identity provider. + // This is useful if you have configured claim mappings that requires specific scopes to be requested beyond the standard OIDC scopes. // // When omitted, no additional scopes are requested. // @@ -561,8 +530,7 @@ type OIDCClientConfig struct { // of platform components and how they interact with // the configured identity providers. type OIDCClientStatus struct { - // componentName is a required field that specifies the name of the platform - // component using the identity provider as an authentication mode. + // componentName is a required field that specifies the name of the platform component using the identity provider as an authentication mode. // It is used in combination with componentNamespace as a unique identifier. // // componentName must not be an empty string ("") and must not exceed 256 characters in length. @@ -572,9 +540,8 @@ type OIDCClientStatus struct { // +required ComponentName string `json:"componentName"` - // componentNamespace is a required field that specifies the namespace in which the - // platform component using the identity provider as an authentication - // mode is running. + // componentNamespace is a required field that specifies the namespace in which the platform component using the identity provider as an authentication mode is running. + // // It is used in combination with componentName as a unique identifier. // // componentNamespace must not be an empty string ("") and must not exceed 63 characters in length. @@ -585,6 +552,7 @@ type OIDCClientStatus struct { ComponentNamespace string `json:"componentNamespace"` // currentOIDCClients is an optional list of clients that the component is currently using. + // // Entries must have unique issuerURL/clientID pairs. // // +listType=map @@ -593,8 +561,7 @@ type OIDCClientStatus struct { // +optional CurrentOIDCClients []OIDCClientReference `json:"currentOIDCClients"` - // consumingUsers is an optional list of ServiceAccounts requiring - // read permissions on the `clientSecret` secret. + // consumingUsers is an optional list of ServiceAccounts requiring read permissions on the `clientSecret` secret. // // consumingUsers must not exceed 5 entries. // @@ -620,8 +587,7 @@ type OIDCClientStatus struct { // OIDCClientReference is a reference to a platform component // client configuration. type OIDCClientReference struct { - // oidcProviderName is a required reference to the 'name' of the identity provider - // configured in 'oidcProviders' that this client is associated with. + // oidcProviderName is a required reference to the 'name' of the identity provider configured in 'oidcProviders' that this client is associated with. // // oidcProviderName must not be an empty string (""). // @@ -629,8 +595,7 @@ type OIDCClientReference struct { // +required OIDCProviderName string `json:"oidcProviderName"` - // issuerURL is a required field that specifies the URL of the identity - // provider that this client is configured to make requests against. + // issuerURL is a required field that specifies the URL of the identity provider that this client is configured to make requests against. // // issuerURL must use the 'https' scheme. // @@ -638,9 +603,7 @@ type OIDCClientReference struct { // +required IssuerURL string `json:"issuerURL"` - // clientID is a required field that specifies the client identifier, from - // the identity provider, that the platform component is using for authentication - // requests made to the identity provider. + // clientID is a required field that specifies the client identifier, from the identity provider, that the platform component is using for authentication requests made to the identity provider. // // clientID must not be empty. // @@ -651,35 +614,52 @@ type OIDCClientReference struct { // +kubebuilder:validation:XValidation:rule="has(self.prefixPolicy) && self.prefixPolicy == 'Prefix' ? (has(self.prefix) && size(self.prefix.prefixString) > 0) : !has(self.prefix)",message="prefix must be set if prefixPolicy is 'Prefix', but must remain unset otherwise" // +union +// +openshift:validation:FeatureGateAwareXValidation:featureGate="",rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDC,rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUIDAndExtraClaimMappings,rule="has(self.claim)",message="claim is required" +// +openshift:validation:FeatureGateAwareXValidation:featureGate=ExternalOIDCWithUpstreamParity,rule="has(self.claim) ? !has(self.expression) : has(self.expression)",message="precisely one of claim or expression must be set" type UsernameClaimMapping struct { - // claim is a required field that configures the JWT token - // claim whose value is assigned to the cluster identity - // field associated with this mapping. + // claim is an optional field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping. + // claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. + // When the ExternalOIDCWithUpstreamParity feature gate is enabled, claim must not be set when expression is set. // // claim must not be an empty string ("") and must not exceed 256 characters. // - // +required + // +optional // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=256 - Claim string `json:"claim"` + Claim string `json:"claim,omitempty"` + + // expression is an optional CEL expression used to derive + // the username from JWT claims. + // + // CEL expressions have access to the token claims + // through a CEL variable, 'claims'. + // + // expression must be at least 1 character and must not exceed 1024 characters in length. + // expression must not be set when claim is set. + // + // +optional + // +openshift:enable:FeatureGate=ExternalOIDCWithUpstreamParity + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Expression string `json:"expression,omitempty"` - // prefixPolicy is an optional field that configures how a prefix should be - // applied to the value of the JWT claim specified in the 'claim' field. + // prefixPolicy is an optional field that configures how a prefix should be applied to the value of the JWT claim specified in the 'claim' field. // // Allowed values are 'Prefix', 'NoPrefix', and omitted (not provided or an empty string). // - // When set to 'Prefix', the value specified in the prefix field will be - // prepended to the value of the JWT claim. + // When set to 'Prefix', the value specified in the prefix field will be prepended to the value of the JWT claim. + // // The prefix field must be set when prefixPolicy is 'Prefix'. // - // When set to 'NoPrefix', no prefix will be prepended to the value - // of the JWT claim. + // When set to 'NoPrefix', no prefix will be prepended to the value of the JWT claim. + // + // When omitted, this means no opinion and the platform is left to choose any prefixes that are applied which is subject to change over time. + // Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim when the claim is not 'email'. // - // When omitted, this means no opinion and the platform is left to choose - // any prefixes that are applied which is subject to change over time. - // Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim - // when the claim is not 'email'. // As an example, consider the following scenario: + // // `prefix` is unset, `issuerURL` is set to `https://myoidc.tld`, // the JWT claims include "username":"userA" and "email":"userA@myoidc.tld", // and `claim` is set to: @@ -691,8 +671,7 @@ type UsernameClaimMapping struct { // +unionDiscriminator PrefixPolicy UsernamePrefixPolicy `json:"prefixPolicy"` - // prefix configures the prefix that should be prepended to the value - // of the JWT claim. + // prefix configures the prefix that should be prepended to the value of the JWT claim. // // prefix must be set when prefixPolicy is set to 'Prefix' and must be unset otherwise. // @@ -701,9 +680,7 @@ type UsernameClaimMapping struct { Prefix *UsernamePrefix `json:"prefix"` } -// UsernamePrefixPolicy configures how prefixes should be applied -// to values extracted from the JWT claims during the process of mapping -// JWT claims to cluster identity attributes. +// UsernamePrefixPolicy configures how prefixes should be applied to values extracted from the JWT claims during the process of mapping JWT claims to cluster identity attributes. // +enum type UsernamePrefixPolicy string @@ -722,9 +699,7 @@ var ( // UsernamePrefix configures the string that should // be used as a prefix for username claim mappings. type UsernamePrefix struct { - // prefixString is a required field that configures the prefix that will - // be applied to cluster identity username attribute - // during the process of mapping JWT claims to cluster identity attributes. + // prefixString is a required field that configures the prefix that will be applied to cluster identity username attribute during the process of mapping JWT claims to cluster identity attributes. // // prefixString must not be an empty string (""). // @@ -738,15 +713,11 @@ type UsernamePrefix struct { type PrefixedClaimMapping struct { TokenClaimMapping `json:",inline"` - // prefix is an optional field that configures the prefix that will be - // applied to the cluster identity attribute during the process of mapping - // JWT claims to cluster identity attributes. + // prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes. // // When omitted (""), no prefix is applied to the cluster identity attribute. // - // Example: if `prefix` is set to "myoidc:" and the `claim` in JWT contains - // an array of strings "a", "b" and "c", the mapping will result in an - // array of string "myoidc:a", "myoidc:b" and "myoidc:c". + // Example: if `prefix` is set to "myoidc:" and the `claim` in JWT contains an array of strings "a", "b" and "c", the mapping will result in an array of string "myoidc:a", "myoidc:b" and "myoidc:c". // // +optional Prefix string `json:"prefix"` @@ -780,19 +751,15 @@ type TokenClaimValidationRule struct { // // Allowed values are "RequiredClaim" and "CEL". // - // When set to 'RequiredClaim', the Kubernetes API server will be configured - // to validate that the incoming JWT contains the required claim and that its - // value matches the required value. + // When set to 'RequiredClaim', the Kubernetes API server will be configured to validate that the incoming JWT contains the required claim and that its value matches the required value. // - // When set to 'CEL', the Kubernetes API server will be configured - // to validate the incoming JWT against the configured CEL expression. + // When set to 'CEL', the Kubernetes API server will be configured to validate the incoming JWT against the configured CEL expression. // +required Type TokenValidationRuleType `json:"type"` // requiredClaim allows configuring a required claim name and its expected value. - // This field is required when `type` is set to RequiredClaim, and must be omitted - // when `type` is set to any other value. The Kubernetes API server uses this field - // to validate if an incoming JWT is valid for this identity provider. + // This field is required when `type` is set to RequiredClaim, and must be omitted when `type` is set to any other value. + // The Kubernetes API server uses this field to validate if an incoming JWT is valid for this identity provider. // // +optional RequiredClaim *TokenRequiredClaim `json:"requiredClaim,omitempty"` @@ -814,10 +781,8 @@ type TokenRequiredClaim struct { // +required Claim string `json:"claim"` - // requiredValue is a required field that configures the value that 'claim' must - // have when taken from the incoming JWT claims. - // If the value in the JWT claims does not match, the token - // will be rejected for authentication. + // requiredValue is a required field that configures the value that 'claim' must have when taken from the incoming JWT claims. + // If the value in the JWT claims does not match, the token will be rejected for authentication. // // requiredValue must not be an empty string (""). // @@ -836,8 +801,7 @@ type TokenClaimValidationCELRule struct { // +required Expression string `json:"expression,omitempty"` - // message is a required human-readable message to be logged by the Kubernetes API server - // if the CEL expression defined in 'expression' fails. + // message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. // message must be at least 1 character in length and must not exceed 256 characters. // +required // +kubebuilder:validation:MinLength=1 @@ -848,8 +812,8 @@ type TokenClaimValidationCELRule struct { // TokenUserValidationRule provides a CEL-based rule used to validate a token subject. // Each rule contains a CEL expression that is evaluated against the token’s claims. type TokenUserValidationRule struct { - // expression is a required CEL expression that performs a validation - // on cluster user identity attributes like username, groups, etc. + // expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc. + // // The expression must evaluate to a boolean value. // When the expression evaluates to 'true', the cluster user identity is considered valid. // When the expression evaluates to 'false', the cluster user identity is not considered valid. @@ -859,8 +823,7 @@ type TokenUserValidationRule struct { // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=1024 Expression string `json:"expression,omitempty"` - // message is a required human-readable message to be logged by the Kubernetes API server - // if the CEL expression defined in 'expression' fails. + // message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. // message must be at least 1 character in length and must not exceed 256 characters. // +required // +kubebuilder:validation:MinLength=1 diff --git a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go index 313ed57a4..369ba1e7a 100644 --- a/vendor/github.com/openshift/api/config/v1/types_infrastructure.go +++ b/vendor/github.com/openshift/api/config/v1/types_infrastructure.go @@ -302,9 +302,10 @@ type PlatformSpec struct { // balancers, dynamic volume provisioning, machine creation and deletion, and // other integrations are enabled. If None, no infrastructure automation is // enabled. Allowed values are "AWS", "Azure", "BareMetal", "GCP", "Libvirt", - // "OpenStack", "VSphere", "oVirt", "KubeVirt", "EquinixMetal", "PowerVS", - // "AlibabaCloud", "Nutanix" and "None". Individual components may not support all platforms, - // and must handle unrecognized platforms as None if they do not support that platform. + // "OpenStack", "VSphere", "oVirt", "IBMCloud", "KubeVirt", "EquinixMetal", + // "PowerVS", "AlibabaCloud", "Nutanix", "External", and "None". Individual + // components may not support all platforms, and must handle unrecognized + // platforms as None if they do not support that platform. // // +unionDiscriminator Type PlatformType `json:"type"` diff --git a/vendor/github.com/openshift/api/config/v1/types_insights.go b/vendor/github.com/openshift/api/config/v1/types_insights.go index b0959881f..710d4303d 100644 --- a/vendor/github.com/openshift/api/config/v1/types_insights.go +++ b/vendor/github.com/openshift/api/config/v1/types_insights.go @@ -13,6 +13,7 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +openshift:api-approved.openshift.io=https://github.com/openshift/api/pull/2448 // +openshift:file-pattern=cvoRunLevel=0000_10,operatorName=config-operator,operatorOrdering=01 // +openshift:enable:FeatureGate=InsightsConfig +// +openshift:capability=Insights // // Compatibility level 1: Stable within a major release for a minimum of 12 months or 3 minor releases (whichever is longer). // +openshift:compatibility-gen:level=1 diff --git a/vendor/github.com/openshift/api/config/v1/types_network.go b/vendor/github.com/openshift/api/config/v1/types_network.go index c0d1602b3..fb8ed2fff 100644 --- a/vendor/github.com/openshift/api/config/v1/types_network.go +++ b/vendor/github.com/openshift/api/config/v1/types_network.go @@ -41,7 +41,7 @@ type Network struct { // As a general rule, this SHOULD NOT be read directly. Instead, you should // consume the NetworkStatus, as it indicates the currently deployed configuration. // Currently, most spec fields are immutable after installation. Please view the individual ones for further details on each. -// +openshift:validation:FeatureGateAwareXValidation:featureGate=NetworkDiagnosticsConfig,rule="!has(self.networkDiagnostics) || !has(self.networkDiagnostics.mode) || self.networkDiagnostics.mode!='Disabled' || !has(self.networkDiagnostics.sourcePlacement) && !has(self.networkDiagnostics.targetPlacement)",message="cannot set networkDiagnostics.sourcePlacement and networkDiagnostics.targetPlacement when networkDiagnostics.mode is Disabled" +// +kubebuilder:validation:XValidation:rule="!has(self.networkDiagnostics) || !has(self.networkDiagnostics.mode) || self.networkDiagnostics.mode!='Disabled' || !has(self.networkDiagnostics.sourcePlacement) && !has(self.networkDiagnostics.targetPlacement)",message="cannot set networkDiagnostics.sourcePlacement and networkDiagnostics.targetPlacement when networkDiagnostics.mode is Disabled" type NetworkSpec struct { // IP address pool to use for pod IPs. // This field is immutable after installation. @@ -85,7 +85,6 @@ type NetworkSpec struct { // the network diagnostics feature will be disabled. // // +optional - // +openshift:enable:FeatureGate=NetworkDiagnosticsConfig NetworkDiagnostics NetworkDiagnostics `json:"networkDiagnostics"` } @@ -119,7 +118,6 @@ type NetworkStatus struct { // +optional // +listType=map // +listMapKey=type - // +openshift:enable:FeatureGate=NetworkDiagnosticsConfig Conditions []metav1.Condition `json:"conditions,omitempty"` } diff --git a/vendor/github.com/openshift/api/config/v1/types_tlssecurityprofile.go b/vendor/github.com/openshift/api/config/v1/types_tlssecurityprofile.go index 1e5189796..48657b089 100644 --- a/vendor/github.com/openshift/api/config/v1/types_tlssecurityprofile.go +++ b/vendor/github.com/openshift/api/config/v1/types_tlssecurityprofile.go @@ -7,9 +7,10 @@ type TLSSecurityProfile struct { // type is one of Old, Intermediate, Modern or Custom. Custom provides the // ability to specify individual TLS security profile parameters. // - // The profiles are currently based on version 5.0 of the Mozilla Server Side TLS - // configuration guidelines (released 2019-06-28) with TLS 1.3 ciphers added for - // forward compatibility. See: https://ssl-config.mozilla.org/guidelines/5.0.json + // The profiles are based on version 5.7 of the Mozilla Server Side TLS + // configuration guidelines. The cipher lists consist of the configuration's + // "ciphersuites" followed by the Go-specific "ciphers" from the guidelines. + // See: https://ssl-config.mozilla.org/guidelines/5.7.json // // The profiles are intent based, so they may change over time as new ciphers are // developed and existing ciphers are found to be insecure. Depending on @@ -22,9 +23,6 @@ type TLSSecurityProfile struct { // old is a TLS profile for use when services need to be accessed by very old // clients or libraries and should be used only as a last resort. // - // The cipher list includes TLS 1.3 ciphers for forward compatibility, followed - // by the "old" profile ciphers. - // // This profile is equivalent to a Custom profile specified as: // minTLSVersion: VersionTLS10 // ciphers: @@ -37,23 +35,15 @@ type TLSSecurityProfile struct { // - ECDHE-RSA-AES256-GCM-SHA384 // - ECDHE-ECDSA-CHACHA20-POLY1305 // - ECDHE-RSA-CHACHA20-POLY1305 - // - DHE-RSA-AES128-GCM-SHA256 - // - DHE-RSA-AES256-GCM-SHA384 - // - DHE-RSA-CHACHA20-POLY1305 // - ECDHE-ECDSA-AES128-SHA256 // - ECDHE-RSA-AES128-SHA256 // - ECDHE-ECDSA-AES128-SHA // - ECDHE-RSA-AES128-SHA - // - ECDHE-ECDSA-AES256-SHA384 - // - ECDHE-RSA-AES256-SHA384 // - ECDHE-ECDSA-AES256-SHA // - ECDHE-RSA-AES256-SHA - // - DHE-RSA-AES128-SHA256 - // - DHE-RSA-AES256-SHA256 // - AES128-GCM-SHA256 // - AES256-GCM-SHA384 // - AES128-SHA256 - // - AES256-SHA256 // - AES128-SHA // - AES256-SHA // - DES-CBC3-SHA @@ -66,9 +56,6 @@ type TLSSecurityProfile struct { // legacy clients and want to remain highly secure while being compatible with // most clients currently in use. // - // The cipher list includes TLS 1.3 ciphers for forward compatibility, followed - // by the "intermediate" profile ciphers. - // // This profile is equivalent to a Custom profile specified as: // minTLSVersion: VersionTLS12 // ciphers: @@ -81,8 +68,6 @@ type TLSSecurityProfile struct { // - ECDHE-RSA-AES256-GCM-SHA384 // - ECDHE-ECDSA-CHACHA20-POLY1305 // - ECDHE-RSA-CHACHA20-POLY1305 - // - DHE-RSA-AES128-GCM-SHA256 - // - DHE-RSA-AES256-GCM-SHA384 // // +optional // +nullable @@ -160,12 +145,14 @@ const ( // TLSProfileSpec is the desired behavior of a TLSSecurityProfile. type TLSProfileSpec struct { // ciphers is used to specify the cipher algorithms that are negotiated - // during the TLS handshake. Operators may remove entries their operands - // do not support. For example, to use DES-CBC3-SHA (yaml): + // during the TLS handshake. Operators may remove entries that their operands + // do not support. For example, to use only ECDHE-RSA-AES128-GCM-SHA256 (yaml): // // ciphers: - // - DES-CBC3-SHA + // - ECDHE-RSA-AES128-GCM-SHA256 // + // TLS 1.3 cipher suites (e.g. TLS_AES_128_GCM_SHA256) are not configurable + // and are always enabled when TLS 1.3 is negotiated. // +listType=atomic Ciphers []string `json:"ciphers"` // minTLSVersion is used to specify the minimal version of the TLS protocol @@ -200,9 +187,11 @@ const ( // TLSProfiles contains a map of TLSProfileType names to TLSProfileSpec. // -// These profiles are based on version 5.0 of the Mozilla Server Side TLS -// configuration guidelines (2019-06-28) with TLS 1.3 cipher suites prepended for -// forward compatibility. See: https://ssl-config.mozilla.org/guidelines/5.0.json +// These profiles are based on version 5.7 of the Mozilla Server Side TLS +// configuration guidelines. See: https://ssl-config.mozilla.org/guidelines/5.7.json +// +// Each Ciphers slice is the configuration's "ciphersuites" followed by the +// Go-specific "ciphers" from the guidelines JSON. // // NOTE: The caller needs to make sure to check that these constants are valid // for their binary. Not all entries map to values for all binaries. In the case @@ -220,23 +209,15 @@ var TLSProfiles = map[TLSProfileType]*TLSProfileSpec{ "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-CHACHA20-POLY1305", "ECDHE-RSA-CHACHA20-POLY1305", - "DHE-RSA-AES128-GCM-SHA256", - "DHE-RSA-AES256-GCM-SHA384", - "DHE-RSA-CHACHA20-POLY1305", "ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDHE-ECDSA-AES128-SHA", "ECDHE-RSA-AES128-SHA", - "ECDHE-ECDSA-AES256-SHA384", - "ECDHE-RSA-AES256-SHA384", "ECDHE-ECDSA-AES256-SHA", "ECDHE-RSA-AES256-SHA", - "DHE-RSA-AES128-SHA256", - "DHE-RSA-AES256-SHA256", "AES128-GCM-SHA256", "AES256-GCM-SHA384", "AES128-SHA256", - "AES256-SHA256", "AES128-SHA", "AES256-SHA", "DES-CBC3-SHA", @@ -254,8 +235,6 @@ var TLSProfiles = map[TLSProfileType]*TLSProfileSpec{ "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-CHACHA20-POLY1305", "ECDHE-RSA-CHACHA20-POLY1305", - "DHE-RSA-AES128-GCM-SHA256", - "DHE-RSA-AES256-GCM-SHA384", }, MinTLSVersion: VersionTLS12, }, diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml index 5d4794e4b..eb7c485e0 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.featuregated-crd-manifests.yaml @@ -6,6 +6,7 @@ apiservers.config.openshift.io: Capability: "" Category: "" FeatureGates: + - KMSEncryption - KMSEncryptionProvider FilenameOperatorName: config-operator FilenameOperatorOrdering: "01" @@ -416,7 +417,7 @@ insightsdatagathers.config.openshift.io: Annotations: {} ApprovedPRNumber: https://github.com/openshift/api/pull/2448 CRDName: insightsdatagathers.config.openshift.io - Capability: "" + Capability: Insights Category: "" FeatureGates: - InsightsConfig @@ -442,8 +443,7 @@ networks.config.openshift.io: CRDName: networks.config.openshift.io Capability: "" Category: "" - FeatureGates: - - NetworkDiagnosticsConfig + FeatureGates: [] FilenameOperatorName: config-operator FilenameOperatorOrdering: "01" FilenameRunLevel: "0000_10" diff --git a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go index e7bc0aebb..621dbbebd 100644 --- a/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/config/v1/zz_generated.swagger_doc_generated.go @@ -407,11 +407,11 @@ func (ExtraMapping) SwaggerDoc() map[string]string { } var map_OIDCClientConfig = map[string]string{ - "": "OIDCClientConfig configures how platform clients interact with identity providers as an authentication method", - "componentName": "componentName is a required field that specifies the name of the platform component being configured to use the identity provider as an authentication mode. It is used in combination with componentNamespace as a unique identifier.\n\ncomponentName must not be an empty string (\"\") and must not exceed 256 characters in length.", - "componentNamespace": "componentNamespace is a required field that specifies the namespace in which the platform component being configured to use the identity provider as an authentication mode is running. It is used in combination with componentName as a unique identifier.\n\ncomponentNamespace must not be an empty string (\"\") and must not exceed 63 characters in length.", + "": "OIDCClientConfig configures how platform clients interact with identity providers as an authentication method.", + "componentName": "componentName is a required field that specifies the name of the platform component being configured to use the identity provider as an authentication mode.\n\nIt is used in combination with componentNamespace as a unique identifier.\n\ncomponentName must not be an empty string (\"\") and must not exceed 256 characters in length.", + "componentNamespace": "componentNamespace is a required field that specifies the namespace in which the platform component being configured to use the identity provider as an authentication mode is running.\n\nIt is used in combination with componentName as a unique identifier.\n\ncomponentNamespace must not be an empty string (\"\") and must not exceed 63 characters in length.", "clientID": "clientID is a required field that configures the client identifier, from the identity provider, that the platform component uses for authentication requests made to the identity provider. The identity provider must accept this identifier for platform components to be able to use the identity provider as an authentication mode.\n\nclientID must not be an empty string (\"\").", - "clientSecret": "clientSecret is an optional field that configures the client secret used by the platform component when making authentication requests to the identity provider.\n\nWhen not specified, no client secret will be used when making authentication requests to the identity provider.\n\nWhen specified, clientSecret references a Secret in the 'openshift-config' namespace that contains the client secret in the 'clientSecret' key of the '.data' field. The client secret will be used when making authentication requests to the identity provider.\n\nPublic clients do not require a client secret but private clients do require a client secret to work with the identity provider.", + "clientSecret": "clientSecret is an optional field that configures the client secret used by the platform component when making authentication requests to the identity provider.\n\nWhen not specified, no client secret will be used when making authentication requests to the identity provider.\n\nWhen specified, clientSecret references a Secret in the 'openshift-config' namespace that contains the client secret in the 'clientSecret' key of the '.data' field.\n\nThe client secret will be used when making authentication requests to the identity provider.\n\nPublic clients do not require a client secret but private clients do require a client secret to work with the identity provider.", "extraScopes": "extraScopes is an optional field that configures the extra scopes that should be requested by the platform component when making authentication requests to the identity provider. This is useful if you have configured claim mappings that requires specific scopes to be requested beyond the standard OIDC scopes.\n\nWhen omitted, no additional scopes are requested.", } @@ -433,8 +433,8 @@ func (OIDCClientReference) SwaggerDoc() map[string]string { var map_OIDCClientStatus = map[string]string{ "": "OIDCClientStatus represents the current state of platform components and how they interact with the configured identity providers.", "componentName": "componentName is a required field that specifies the name of the platform component using the identity provider as an authentication mode. It is used in combination with componentNamespace as a unique identifier.\n\ncomponentName must not be an empty string (\"\") and must not exceed 256 characters in length.", - "componentNamespace": "componentNamespace is a required field that specifies the namespace in which the platform component using the identity provider as an authentication mode is running. It is used in combination with componentName as a unique identifier.\n\ncomponentNamespace must not be an empty string (\"\") and must not exceed 63 characters in length.", - "currentOIDCClients": "currentOIDCClients is an optional list of clients that the component is currently using. Entries must have unique issuerURL/clientID pairs.", + "componentNamespace": "componentNamespace is a required field that specifies the namespace in which the platform component using the identity provider as an authentication mode is running.\n\nIt is used in combination with componentName as a unique identifier.\n\ncomponentNamespace must not be an empty string (\"\") and must not exceed 63 characters in length.", + "currentOIDCClients": "currentOIDCClients is an optional list of clients that the component is currently using.\n\nEntries must have unique issuerURL/clientID pairs.", "consumingUsers": "consumingUsers is an optional list of ServiceAccounts requiring read permissions on the `clientSecret` secret.\n\nconsumingUsers must not exceed 5 entries.", "conditions": "conditions are used to communicate the state of the `oidcClients` entry.\n\nSupported conditions include Available, Degraded and Progressing.\n\nIf Available is true, the component is successfully using the configured client. If Degraded is true, that means something has gone wrong trying to handle the client configuration. If Progressing is true, that means the component is taking some action related to the `oidcClients` entry.", } @@ -458,7 +458,7 @@ func (OIDCProvider) SwaggerDoc() map[string]string { var map_PrefixedClaimMapping = map[string]string{ "": "PrefixedClaimMapping configures a claim mapping that allows for an optional prefix.", - "prefix": "prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes.\n\nWhen omitted (\"\"), no prefix is applied to the cluster identity attribute.\n\nExample: if `prefix` is set to \"myoidc:\" and the `claim` in JWT contains an array of strings \"a\", \"b\" and \"c\", the mapping will result in an array of string \"myoidc:a\", \"myoidc:b\" and \"myoidc:c\".", + "prefix": "prefix is an optional field that configures the prefix that will be applied to the cluster identity attribute during the process of mapping JWT claims to cluster identity attributes.\n\nWhen omitted (\"\"), no prefix is applied to the cluster identity attribute.\n\nExample: if `prefix` is set to \"myoidc:\" and the `claim` in JWT contains an array of strings \"a\", \"b\" and \"c\", the mapping will result in an array of string \"myoidc:a\", \"myoidc:b\" and \"myoidc:c\".", } func (PrefixedClaimMapping) SwaggerDoc() map[string]string { @@ -466,8 +466,9 @@ func (PrefixedClaimMapping) SwaggerDoc() map[string]string { } var map_TokenClaimMapping = map[string]string{ - "": "TokenClaimMapping allows specifying a JWT token claim to be used when mapping claims from an authentication token to cluster identities.", - "claim": "claim is a required field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping.", + "": "TokenClaimMapping allows specifying a JWT token claim to be used when mapping claims from an authentication token to cluster identities.", + "claim": "claim is an optional field for specifying the JWT token claim that is used in the mapping. The value of this claim will be assigned to the field in which this mapping is associated. claim must not exceed 256 characters in length. When set to the empty string `\"\"`, this means that no named claim should be used for the group mapping. claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled.", + "expression": "expression is an optional CEL expression used to derive group values from JWT claims.\n\nCEL expressions have access to the token claims through a CEL variable, 'claims'.\n\nexpression must be at least 1 character and must not exceed 1024 characters in length .\n\nWhen specified, claim must not be set or be explicitly set to the empty string (`\"\"`).", } func (TokenClaimMapping) SwaggerDoc() map[string]string { @@ -476,9 +477,9 @@ func (TokenClaimMapping) SwaggerDoc() map[string]string { var map_TokenClaimMappings = map[string]string{ "username": "username is a required field that configures how the username of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider.", - "groups": "groups is an optional field that configures how the groups of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider. When referencing a claim, if the claim is present in the JWT token, its value must be a list of groups separated by a comma (','). For example - '\"example\"' and '\"exampleOne\", \"exampleTwo\", \"exampleThree\"' are valid claim values.", - "uid": "uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity.\n\nWhen using uid.claim to specify the claim it must be a single string value. When using uid.expression the expression must result in a single string value.\n\nWhen omitted, this means the user has no opinion and the platform is left to choose a default, which is subject to change over time. The current default is to use the 'sub' claim.", - "extra": "extra is an optional field for configuring the mappings used to construct the extra attribute for the cluster identity. When omitted, no extra attributes will be present on the cluster identity. key values for extra mappings must be unique. A maximum of 32 extra attribute mappings may be provided.", + "groups": "groups is an optional field that configures how the groups of a cluster identity should be constructed from the claims in a JWT token issued by the identity provider.\n\nWhen referencing a claim, if the claim is present in the JWT token, its value must be a list of groups separated by a comma (',').\n\nFor example - '\"example\"' and '\"exampleOne\", \"exampleTwo\", \"exampleThree\"' are valid claim values.", + "uid": "uid is an optional field for configuring the claim mapping used to construct the uid for the cluster identity.\n\nWhen using uid.claim to specify the claim it must be a single string value. When using uid.expression the expression must result in a single string value.\n\nWhen omitted, this means the user has no opinion and the platform is left to choose a default, which is subject to change over time.\n\nThe current default is to use the 'sub' claim.", + "extra": "extra is an optional field for configuring the mappings used to construct the extra attribute for the cluster identity. When omitted, no extra attributes will be present on the cluster identity.\n\nkey values for extra mappings must be unique. A maximum of 32 extra attribute mappings may be provided.", } func (TokenClaimMappings) SwaggerDoc() map[string]string { @@ -519,7 +520,7 @@ var map_TokenIssuer = map[string]string{ "issuerURL": "issuerURL is a required field that configures the URL used to issue tokens by the identity provider. The Kubernetes API server determines how authentication tokens should be handled by matching the 'iss' claim in the JWT to the issuerURL of configured identity providers.\n\nMust be at least 1 character and must not exceed 512 characters in length. Must be a valid URL that uses the 'https' scheme and does not contain a query, fragment or user.", "audiences": "audiences is a required field that configures the acceptable audiences the JWT token, issued by the identity provider, must be issued to. At least one of the entries must match the 'aud' claim in the JWT token.\n\naudiences must contain at least one entry and must not exceed ten entries.", "issuerCertificateAuthority": "issuerCertificateAuthority is an optional field that configures the certificate authority, used by the Kubernetes API server, to validate the connection to the identity provider when fetching discovery information.\n\nWhen not specified, the system trust is used.\n\nWhen specified, it must reference a ConfigMap in the openshift-config namespace containing the PEM-encoded CA certificates under the 'ca-bundle.crt' key in the data field of the ConfigMap.", - "discoveryURL": "discoveryURL is an optional field that, if specified, overrides the default discovery endpoint used to retrieve OIDC configuration metadata. By default, the discovery URL is derived from `issuerURL` as \"{issuerURL}/.well-known/openid-configuration\".\n\nThe discoveryURL must be a valid absolute HTTPS URL. It must not contain query parameters, user information, or fragments. Additionally, it must differ from the value of `url` (ignoring trailing slashes). The discoveryURL value must be at least 1 character long and no longer than 2048 characters.", + "discoveryURL": "discoveryURL is an optional field that, if specified, overrides the default discovery endpoint used to retrieve OIDC configuration metadata. By default, the discovery URL is derived from `issuerURL` as \"{issuerURL}/.well-known/openid-configuration\".\n\nThe discoveryURL must be a valid absolute HTTPS URL. It must not contain query parameters, user information, or fragments. Additionally, it must differ from the value of `issuerURL` (ignoring trailing slashes). The discoveryURL value must be at least 1 character long and no longer than 2048 characters.", } func (TokenIssuer) SwaggerDoc() map[string]string { @@ -537,7 +538,7 @@ func (TokenRequiredClaim) SwaggerDoc() map[string]string { var map_TokenUserValidationRule = map[string]string{ "": "TokenUserValidationRule provides a CEL-based rule used to validate a token subject. Each rule contains a CEL expression that is evaluated against the token’s claims.", - "expression": "expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc. The expression must evaluate to a boolean value. When the expression evaluates to 'true', the cluster user identity is considered valid. When the expression evaluates to 'false', the cluster user identity is not considered valid. expression must be at least 1 character in length and must not exceed 1024 characters.", + "expression": "expression is a required CEL expression that performs a validation on cluster user identity attributes like username, groups, etc.\n\nThe expression must evaluate to a boolean value. When the expression evaluates to 'true', the cluster user identity is considered valid. When the expression evaluates to 'false', the cluster user identity is not considered valid. expression must be at least 1 character in length and must not exceed 1024 characters.", "message": "message is a required human-readable message to be logged by the Kubernetes API server if the CEL expression defined in 'expression' fails. message must be at least 1 character in length and must not exceed 256 characters.", } @@ -546,8 +547,9 @@ func (TokenUserValidationRule) SwaggerDoc() map[string]string { } var map_UsernameClaimMapping = map[string]string{ - "claim": "claim is a required field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping.\n\nclaim must not be an empty string (\"\") and must not exceed 256 characters.", - "prefixPolicy": "prefixPolicy is an optional field that configures how a prefix should be applied to the value of the JWT claim specified in the 'claim' field.\n\nAllowed values are 'Prefix', 'NoPrefix', and omitted (not provided or an empty string).\n\nWhen set to 'Prefix', the value specified in the prefix field will be prepended to the value of the JWT claim. The prefix field must be set when prefixPolicy is 'Prefix'.\n\nWhen set to 'NoPrefix', no prefix will be prepended to the value of the JWT claim.\n\nWhen omitted, this means no opinion and the platform is left to choose any prefixes that are applied which is subject to change over time. Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim when the claim is not 'email'. As an example, consider the following scenario:\n `prefix` is unset, `issuerURL` is set to `https://myoidc.tld`,\n the JWT claims include \"username\":\"userA\" and \"email\":\"userA@myoidc.tld\",\n and `claim` is set to:\n - \"username\": the mapped value will be \"https://myoidc.tld#userA\"\n - \"email\": the mapped value will be \"userA@myoidc.tld\"", + "claim": "claim is an optional field that configures the JWT token claim whose value is assigned to the cluster identity field associated with this mapping. claim is required when the ExternalOIDCWithUpstreamParity feature gate is not enabled. When the ExternalOIDCWithUpstreamParity feature gate is enabled, claim must not be set when expression is set.\n\nclaim must not be an empty string (\"\") and must not exceed 256 characters.", + "expression": "expression is an optional CEL expression used to derive the username from JWT claims.\n\nCEL expressions have access to the token claims through a CEL variable, 'claims'.\n\nexpression must be at least 1 character and must not exceed 1024 characters in length. expression must not be set when claim is set.", + "prefixPolicy": "prefixPolicy is an optional field that configures how a prefix should be applied to the value of the JWT claim specified in the 'claim' field.\n\nAllowed values are 'Prefix', 'NoPrefix', and omitted (not provided or an empty string).\n\nWhen set to 'Prefix', the value specified in the prefix field will be prepended to the value of the JWT claim.\n\nThe prefix field must be set when prefixPolicy is 'Prefix'.\n\nWhen set to 'NoPrefix', no prefix will be prepended to the value of the JWT claim.\n\nWhen omitted, this means no opinion and the platform is left to choose any prefixes that are applied which is subject to change over time. Currently, the platform prepends `{issuerURL}#` to the value of the JWT claim when the claim is not 'email'.\n\nAs an example, consider the following scenario:\n\n `prefix` is unset, `issuerURL` is set to `https://myoidc.tld`,\n the JWT claims include \"username\":\"userA\" and \"email\":\"userA@myoidc.tld\",\n and `claim` is set to:\n - \"username\": the mapped value will be \"https://myoidc.tld#userA\"\n - \"email\": the mapped value will be \"userA@myoidc.tld\"", "prefix": "prefix configures the prefix that should be prepended to the value of the JWT claim.\n\nprefix must be set when prefixPolicy is set to 'Prefix' and must be unset otherwise.", } @@ -1924,7 +1926,7 @@ func (OvirtPlatformStatus) SwaggerDoc() map[string]string { var map_PlatformSpec = map[string]string{ "": "PlatformSpec holds the desired state specific to the underlying infrastructure provider of the current cluster. Since these are used at spec-level for the underlying cluster, it is supposed that only one of the spec structs is set.", - "type": "type is the underlying infrastructure provider for the cluster. This value controls whether infrastructure automation such as service load balancers, dynamic volume provisioning, machine creation and deletion, and other integrations are enabled. If None, no infrastructure automation is enabled. Allowed values are \"AWS\", \"Azure\", \"BareMetal\", \"GCP\", \"Libvirt\", \"OpenStack\", \"VSphere\", \"oVirt\", \"KubeVirt\", \"EquinixMetal\", \"PowerVS\", \"AlibabaCloud\", \"Nutanix\" and \"None\". Individual components may not support all platforms, and must handle unrecognized platforms as None if they do not support that platform.", + "type": "type is the underlying infrastructure provider for the cluster. This value controls whether infrastructure automation such as service load balancers, dynamic volume provisioning, machine creation and deletion, and other integrations are enabled. If None, no infrastructure automation is enabled. Allowed values are \"AWS\", \"Azure\", \"BareMetal\", \"GCP\", \"Libvirt\", \"OpenStack\", \"VSphere\", \"oVirt\", \"IBMCloud\", \"KubeVirt\", \"EquinixMetal\", \"PowerVS\", \"AlibabaCloud\", \"Nutanix\", \"External\", and \"None\". Individual components may not support all platforms, and must handle unrecognized platforms as None if they do not support that platform.", "aws": "aws contains settings specific to the Amazon Web Services infrastructure provider.", "azure": "azure contains settings specific to the Azure infrastructure provider.", "gcp": "gcp contains settings specific to the Google Cloud Platform infrastructure provider.", @@ -3004,7 +3006,7 @@ func (OldTLSProfile) SwaggerDoc() map[string]string { var map_TLSProfileSpec = map[string]string{ "": "TLSProfileSpec is the desired behavior of a TLSSecurityProfile.", - "ciphers": "ciphers is used to specify the cipher algorithms that are negotiated during the TLS handshake. Operators may remove entries their operands do not support. For example, to use DES-CBC3-SHA (yaml):\n\n ciphers:\n - DES-CBC3-SHA", + "ciphers": "ciphers is used to specify the cipher algorithms that are negotiated during the TLS handshake. Operators may remove entries that their operands do not support. For example, to use only ECDHE-RSA-AES128-GCM-SHA256 (yaml):\n\n ciphers:\n - ECDHE-RSA-AES128-GCM-SHA256\n\nTLS 1.3 cipher suites (e.g. TLS_AES_128_GCM_SHA256) are not configurable and are always enabled when TLS 1.3 is negotiated.", "minTLSVersion": "minTLSVersion is used to specify the minimal version of the TLS protocol that is negotiated during the TLS handshake. For example, to use TLS versions 1.1, 1.2 and 1.3 (yaml):\n\n minTLSVersion: VersionTLS11", } @@ -3014,9 +3016,9 @@ func (TLSProfileSpec) SwaggerDoc() map[string]string { var map_TLSSecurityProfile = map[string]string{ "": "TLSSecurityProfile defines the schema for a TLS security profile. This object is used by operators to apply TLS security settings to operands.", - "type": "type is one of Old, Intermediate, Modern or Custom. Custom provides the ability to specify individual TLS security profile parameters.\n\nThe profiles are currently based on version 5.0 of the Mozilla Server Side TLS configuration guidelines (released 2019-06-28) with TLS 1.3 ciphers added for forward compatibility. See: https://ssl-config.mozilla.org/guidelines/5.0.json\n\nThe profiles are intent based, so they may change over time as new ciphers are developed and existing ciphers are found to be insecure. Depending on precisely which ciphers are available to a process, the list may be reduced.", - "old": "old is a TLS profile for use when services need to be accessed by very old clients or libraries and should be used only as a last resort.\n\nThe cipher list includes TLS 1.3 ciphers for forward compatibility, followed by the \"old\" profile ciphers.\n\nThis profile is equivalent to a Custom profile specified as:\n minTLSVersion: VersionTLS10\n ciphers:\n - TLS_AES_128_GCM_SHA256\n - TLS_AES_256_GCM_SHA384\n - TLS_CHACHA20_POLY1305_SHA256\n - ECDHE-ECDSA-AES128-GCM-SHA256\n - ECDHE-RSA-AES128-GCM-SHA256\n - ECDHE-ECDSA-AES256-GCM-SHA384\n - ECDHE-RSA-AES256-GCM-SHA384\n - ECDHE-ECDSA-CHACHA20-POLY1305\n - ECDHE-RSA-CHACHA20-POLY1305\n - DHE-RSA-AES128-GCM-SHA256\n - DHE-RSA-AES256-GCM-SHA384\n - DHE-RSA-CHACHA20-POLY1305\n - ECDHE-ECDSA-AES128-SHA256\n - ECDHE-RSA-AES128-SHA256\n - ECDHE-ECDSA-AES128-SHA\n - ECDHE-RSA-AES128-SHA\n - ECDHE-ECDSA-AES256-SHA384\n - ECDHE-RSA-AES256-SHA384\n - ECDHE-ECDSA-AES256-SHA\n - ECDHE-RSA-AES256-SHA\n - DHE-RSA-AES128-SHA256\n - DHE-RSA-AES256-SHA256\n - AES128-GCM-SHA256\n - AES256-GCM-SHA384\n - AES128-SHA256\n - AES256-SHA256\n - AES128-SHA\n - AES256-SHA\n - DES-CBC3-SHA", - "intermediate": "intermediate is a TLS profile for use when you do not need compatibility with legacy clients and want to remain highly secure while being compatible with most clients currently in use.\n\nThe cipher list includes TLS 1.3 ciphers for forward compatibility, followed by the \"intermediate\" profile ciphers.\n\nThis profile is equivalent to a Custom profile specified as:\n minTLSVersion: VersionTLS12\n ciphers:\n - TLS_AES_128_GCM_SHA256\n - TLS_AES_256_GCM_SHA384\n - TLS_CHACHA20_POLY1305_SHA256\n - ECDHE-ECDSA-AES128-GCM-SHA256\n - ECDHE-RSA-AES128-GCM-SHA256\n - ECDHE-ECDSA-AES256-GCM-SHA384\n - ECDHE-RSA-AES256-GCM-SHA384\n - ECDHE-ECDSA-CHACHA20-POLY1305\n - ECDHE-RSA-CHACHA20-POLY1305\n - DHE-RSA-AES128-GCM-SHA256\n - DHE-RSA-AES256-GCM-SHA384", + "type": "type is one of Old, Intermediate, Modern or Custom. Custom provides the ability to specify individual TLS security profile parameters.\n\nThe profiles are based on version 5.7 of the Mozilla Server Side TLS configuration guidelines. The cipher lists consist of the configuration's \"ciphersuites\" followed by the Go-specific \"ciphers\" from the guidelines. See: https://ssl-config.mozilla.org/guidelines/5.7.json\n\nThe profiles are intent based, so they may change over time as new ciphers are developed and existing ciphers are found to be insecure. Depending on precisely which ciphers are available to a process, the list may be reduced.", + "old": "old is a TLS profile for use when services need to be accessed by very old clients or libraries and should be used only as a last resort.\n\nThis profile is equivalent to a Custom profile specified as:\n minTLSVersion: VersionTLS10\n ciphers:\n - TLS_AES_128_GCM_SHA256\n - TLS_AES_256_GCM_SHA384\n - TLS_CHACHA20_POLY1305_SHA256\n - ECDHE-ECDSA-AES128-GCM-SHA256\n - ECDHE-RSA-AES128-GCM-SHA256\n - ECDHE-ECDSA-AES256-GCM-SHA384\n - ECDHE-RSA-AES256-GCM-SHA384\n - ECDHE-ECDSA-CHACHA20-POLY1305\n - ECDHE-RSA-CHACHA20-POLY1305\n - ECDHE-ECDSA-AES128-SHA256\n - ECDHE-RSA-AES128-SHA256\n - ECDHE-ECDSA-AES128-SHA\n - ECDHE-RSA-AES128-SHA\n - ECDHE-ECDSA-AES256-SHA\n - ECDHE-RSA-AES256-SHA\n - AES128-GCM-SHA256\n - AES256-GCM-SHA384\n - AES128-SHA256\n - AES128-SHA\n - AES256-SHA\n - DES-CBC3-SHA", + "intermediate": "intermediate is a TLS profile for use when you do not need compatibility with legacy clients and want to remain highly secure while being compatible with most clients currently in use.\n\nThis profile is equivalent to a Custom profile specified as:\n minTLSVersion: VersionTLS12\n ciphers:\n - TLS_AES_128_GCM_SHA256\n - TLS_AES_256_GCM_SHA384\n - TLS_CHACHA20_POLY1305_SHA256\n - ECDHE-ECDSA-AES128-GCM-SHA256\n - ECDHE-RSA-AES128-GCM-SHA256\n - ECDHE-ECDSA-AES256-GCM-SHA384\n - ECDHE-RSA-AES256-GCM-SHA384\n - ECDHE-ECDSA-CHACHA20-POLY1305\n - ECDHE-RSA-CHACHA20-POLY1305", "modern": "modern is a TLS security profile for use with clients that support TLS 1.3 and do not need backward compatibility for older clients.\n\nThis profile is equivalent to a Custom profile specified as:\n minTLSVersion: VersionTLS13\n ciphers:\n - TLS_AES_128_GCM_SHA256\n - TLS_AES_256_GCM_SHA384\n - TLS_CHACHA20_POLY1305_SHA256", "custom": "custom is a user-defined TLS security profile. Be extremely careful using a custom profile as invalid configurations can be catastrophic. An example custom profile looks like this:\n\n minTLSVersion: VersionTLS11\n ciphers:\n - ECDHE-ECDSA-CHACHA20-POLY1305\n - ECDHE-RSA-CHACHA20-POLY1305\n - ECDHE-RSA-AES128-GCM-SHA256\n - ECDHE-ECDSA-AES128-GCM-SHA256", } diff --git a/vendor/github.com/openshift/api/operator/v1/types_network.go b/vendor/github.com/openshift/api/operator/v1/types_network.go index 111240eec..1cf56f549 100644 --- a/vendor/github.com/openshift/api/operator/v1/types_network.go +++ b/vendor/github.com/openshift/api/operator/v1/types_network.go @@ -54,7 +54,7 @@ type NetworkList struct { // NetworkSpec is the top-level network configuration object. // +kubebuilder:validation:XValidation:rule="!has(self.defaultNetwork) || !has(self.defaultNetwork.ovnKubernetesConfig) || !has(self.defaultNetwork.ovnKubernetesConfig.gatewayConfig) || !has(self.defaultNetwork.ovnKubernetesConfig.gatewayConfig.ipForwarding) || self.defaultNetwork.ovnKubernetesConfig.gatewayConfig.ipForwarding == oldSelf.defaultNetwork.ovnKubernetesConfig.gatewayConfig.ipForwarding || self.defaultNetwork.ovnKubernetesConfig.gatewayConfig.ipForwarding == 'Restricted' || self.defaultNetwork.ovnKubernetesConfig.gatewayConfig.ipForwarding == 'Global'",message="invalid value for IPForwarding, valid values are 'Restricted' or 'Global'" -// +openshift:validation:FeatureGateAwareXValidation:featureGate=RouteAdvertisements,rule="(has(self.additionalRoutingCapabilities) && ('FRR' in self.additionalRoutingCapabilities.providers)) || !has(self.defaultNetwork) || !has(self.defaultNetwork.ovnKubernetesConfig) || !has(self.defaultNetwork.ovnKubernetesConfig.routeAdvertisements) || self.defaultNetwork.ovnKubernetesConfig.routeAdvertisements != 'Enabled'",message="Route advertisements cannot be Enabled if 'FRR' routing capability provider is not available" +// +kubebuilder:validation:XValidation:rule="(has(self.additionalRoutingCapabilities) && ('FRR' in self.additionalRoutingCapabilities.providers)) || !has(self.defaultNetwork) || !has(self.defaultNetwork.ovnKubernetesConfig) || !has(self.defaultNetwork.ovnKubernetesConfig.routeAdvertisements) || self.defaultNetwork.ovnKubernetesConfig.routeAdvertisements != 'Enabled'",message="Route advertisements cannot be Enabled if 'FRR' routing capability provider is not available" type NetworkSpec struct { OperatorSpec `json:",inline"` @@ -136,7 +136,6 @@ type NetworkSpec struct { // capabilities acquired through the enablement of these components but may // require specific configuration on their side to do so; refer to their // respective documentation and configuration options. - // +openshift:enable:FeatureGate=AdditionalRoutingCapabilities // +optional AdditionalRoutingCapabilities *AdditionalRoutingCapabilities `json:"additionalRoutingCapabilities,omitempty"` } @@ -157,7 +156,7 @@ const ( ) // NetworkMigration represents the cluster network migration configuration. -// +openshift:validation:FeatureGateAwareXValidation:featureGate=NetworkLiveMigration,rule="!has(self.mtu) || !has(self.networkType) || self.networkType == \"\" || has(self.mode) && self.mode == 'Live'",message="networkType migration in mode other than 'Live' may not be configured at the same time as mtu migration" +// +kubebuilder:validation:XValidation:rule="!has(self.mtu) || !has(self.networkType) || self.networkType == \"\" || has(self.mode) && self.mode == 'Live'",message="networkType migration in mode other than 'Live' may not be configured at the same time as mtu migration" type NetworkMigration struct { // mtu contains the MTU migration configuration. Set this to allow changing // the MTU values for the default network. If unset, the operation of @@ -465,7 +464,6 @@ type OVNKubernetesConfig struct { // means the user has no opinion and the platform is left to choose // reasonable defaults. These defaults are subject to change over time. The // current default is "Disabled". - // +openshift:enable:FeatureGate=RouteAdvertisements // +optional RouteAdvertisements RouteAdvertisementsEnablement `json:"routeAdvertisements,omitempty"` } diff --git a/vendor/github.com/openshift/api/operator/v1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/api/operator/v1/zz_generated.featuregated-crd-manifests.yaml index e7c94e286..51a758804 100644 --- a/vendor/github.com/openshift/api/operator/v1/zz_generated.featuregated-crd-manifests.yaml +++ b/vendor/github.com/openshift/api/operator/v1/zz_generated.featuregated-crd-manifests.yaml @@ -327,10 +327,7 @@ networks.operator.openshift.io: CRDName: networks.operator.openshift.io Capability: "" Category: "" - FeatureGates: - - AdditionalRoutingCapabilities - - NetworkLiveMigration - - RouteAdvertisements + FeatureGates: [] FilenameOperatorName: network FilenameOperatorOrdering: "01" FilenameRunLevel: "0000_70" diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azure.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azure.go index f76033535..c99523cee 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azure.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azure.go @@ -358,6 +358,12 @@ type AzureNodePoolOSDisk struct { // your own (BYO) cloud infrastructure resources. For example, resources like a resource group, a subnet, or a vnet // would be pre-created and then their names would be used respectively in the ResourceGroupName, SubnetName, VnetName // fields of the Hosted Cluster CR. An existing cloud resource is expected to exist under the same SubscriptionID. +// +// +kubebuilder:validation:XValidation:rule="has(self.private) == has(oldSelf.private)",message="private cannot be added or removed after cluster creation" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.topology) || has(self.topology)",message="topology cannot be removed once set" +// +kubebuilder:validation:XValidation:rule="!has(self.topology) || !has(oldSelf.topology) || (self.topology == 'Public') == (oldSelf.topology == 'Public')",message="transitions between Public and non-Public topology are not supported" +// +kubebuilder:validation:XValidation:rule="!has(self.topology) || ((self.topology == 'Private' || self.topology == 'PublicAndPrivate') ? has(self.private) : !has(self.private))",message="private is required when topology is Private or PublicAndPrivate, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="!has(self.private) || self.private.type != 'PrivateLink' || self.azureAuthenticationConfig.azureAuthenticationConfigType != 'WorkloadIdentities' || has(self.azureAuthenticationConfig.workloadIdentities.controlPlaneOperator)",message="workloadIdentities.controlPlaneOperator is required when Private Link is configured with WorkloadIdentities authentication" type AzurePlatformSpec struct { // cloud is the cloud environment identifier, valid values could be found here: https://github.com/Azure/go-autorest/blob/4c0e21ca2bbb3251fe7853e6f9df6397f53dd419/autorest/azure/environments.go#L33 // @@ -462,6 +468,29 @@ type AzurePlatformSpec struct { // +required // +kubebuilder:validation:MaxLength=255 TenantID string `json:"tenantID"` + + // topology specifies the network topology of the API server endpoint for the hosted cluster. + // - Public: The API server is accessible only via a public endpoint. + // - PublicAndPrivate: The API server is accessible via both public and private endpoints. + // - Private: The API server is accessible only via a private endpoint. + // When omitted, this means no opinion and the platform is left to choose a reasonable + // default, which is subject to change over time. The current default is Public. + // This field must be set explicitly for self-hosted environments (WorkloadIdentities). + // Transitions between PublicAndPrivate and Private are allowed after creation. + // Transitions from Public to non-Public (or vice versa) are not allowed. + // When set to Private or PublicAndPrivate, the private field must be provided. + // + // +optional + Topology AzureTopologyType `json:"topology,omitempty"` + + // private configures private connectivity to the hosted cluster's API server. + // This field is required when topology is Private or PublicAndPrivate, and must + // not be set when topology is Public. + // Once set at cluster creation, this field cannot be removed, and it cannot be + // added to an existing cluster that was created without it. + // + // +optional + Private AzurePrivateSpec `json:"private,omitzero"` } // objectEncoding represents the encoding for the Azure Key Vault secret containing the certificate related to @@ -554,6 +583,11 @@ type AzureWorkloadIdentities struct { // workload identity authentication. // +required Network WorkloadIdentity `json:"network"` + + // controlPlaneOperator is the client ID of a federated managed identity, associated with control-plane-operator, + // used in workload identity authentication for Azure Private Link Service operations. + // +optional + ControlPlaneOperator WorkloadIdentity `json:"controlPlaneOperator,omitzero"` } // ManagedIdentity contains the client ID, and its certificate name, of a managed identity. This managed identity is @@ -603,6 +637,85 @@ type WorkloadIdentity struct { ClientID AzureClientID `json:"clientID"` } +// AzureTopologyType specifies the network topology of the Azure API server endpoint. +// +kubebuilder:validation:Enum=Public;PublicAndPrivate;Private +type AzureTopologyType string + +const ( + // AzureTopologyPublic indicates the API server is accessible only via a public endpoint. + AzureTopologyPublic AzureTopologyType = "Public" + // AzureTopologyPublicAndPrivate indicates the API server is accessible via both public and private endpoints. + AzureTopologyPublicAndPrivate AzureTopologyType = "PublicAndPrivate" + // AzureTopologyPrivate indicates the API server is accessible only via a private endpoint. + AzureTopologyPrivate AzureTopologyType = "Private" +) + +// AzurePrivateType specifies the type of private connectivity mechanism used for the Azure +// hosted cluster's API server. This acts as the discriminator for the AzurePrivateSpec union. +// +// +kubebuilder:validation:Enum=PrivateLink +type AzurePrivateType string + +const ( + // AzurePrivateTypePrivateLink specifies private connectivity using Azure Private Link Service. + // In this mode, the operator creates a Private Link Service backed by the management cluster's + // internal load balancer, and a Private Endpoint in the guest VNet for private API server access. + AzurePrivateTypePrivateLink AzurePrivateType = "PrivateLink" +) + +// AzurePrivateSpec configures private connectivity to an Azure hosted cluster's API server. +// It is a discriminated union keyed on the type field, which selects the private connectivity +// mechanism. Currently only PrivateLink is supported; additional mechanisms (e.g., Swift) may +// be added in the future. +// +// +kubebuilder:validation:XValidation:rule="self.type != 'PrivateLink' ? !has(self.privateLink) : true",message="privateLink is forbidden when type is not PrivateLink" +// +union +type AzurePrivateSpec struct { + // type specifies the private connectivity mechanism used for the hosted cluster's API server. + // "PrivateLink" selects Azure Private Link Service for private API server access. + // This field is immutable once set. + // + // +unionDiscriminator + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + Type AzurePrivateType `json:"type,omitempty"` + + // privateLink configures Azure Private Link Service for private API server access. + // This field is required when type is "PrivateLink" and must not be set otherwise. + // + // +optional + // +unionMember + PrivateLink AzurePrivateLinkSpec `json:"privateLink,omitzero"` +} + +// AzurePrivateLinkSpec configures Azure Private Link Service connectivity. +// +kubebuilder:validation:MinProperties=1 +type AzurePrivateLinkSpec struct { + // natSubnetID is the Azure resource ID of the subnet used for Private Link Service NAT IP allocation. + // This subnet must have privateLinkServiceNetworkPolicies disabled. + // If not provided, the controller will auto-create a NAT subnet in the HC's VNet. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} + // The maximum length is 355 characters. + // + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="natSubnetID is immutable once set" + NATSubnetID AzureSubnetResourceID `json:"natSubnetID,omitempty"` + + // additionalAllowedSubscriptions is an optional list of additional Azure subscription IDs + // permitted to create Private Endpoints to the Private Link Service. The guest cluster's + // own subscription is always automatically allowed, so it does not need to be listed here. + // Each item must be a valid UUID consisting of lowercase hexadecimal characters and hyphens, + // in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + // (e.g., "550e8400-e29b-41d4-a716-446655440000"). A maximum of 50 subscriptions may be specified. + // + // +optional + // +listType=set + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=50 + AdditionalAllowedSubscriptions []AzureSubscriptionID `json:"additionalAllowedSubscriptions,omitempty"` +} + // ControlPlaneManagedIdentities contains the managed identities on the HCP control plane needing to authenticate with // Azure's API. type ControlPlaneManagedIdentities struct { diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azureprivatelinkservice_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azureprivatelinkservice_types.go new file mode 100644 index 000000000..307b9567a --- /dev/null +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/azureprivatelinkservice_types.go @@ -0,0 +1,309 @@ +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + SchemeBuilder.Register(func(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &AzurePrivateLinkService{}, + &AzurePrivateLinkServiceList{}, + ) + return nil + }) +} + +// The following are condition types and reasons for AzurePrivateLinkService. +const ( + // AzurePrivateLinkServiceAvailable indicates overall PLS infrastructure availability + AzurePrivateLinkServiceAvailable ConditionType = "AzurePrivateLinkServiceAvailable" + + // AzureInternalLoadBalancerAvailable indicates the ILB has been provisioned with a frontend IP + AzureInternalLoadBalancerAvailable ConditionType = "AzureInternalLoadBalancerAvailable" + + // AzurePLSCreated indicates the Azure Private Link Service has been created in the management cluster resource group + AzurePLSCreated ConditionType = "AzurePLSCreated" + + // AzurePrivateEndpointAvailable indicates the Private Endpoint has been created in the guest VNet + AzurePrivateEndpointAvailable ConditionType = "AzurePrivateEndpointAvailable" + + // AzurePrivateDNSAvailable indicates the Private DNS zone and A records have been created + AzurePrivateDNSAvailable ConditionType = "AzurePrivateDNSAvailable" + + AzurePLSSuccessReason string = "AzureSuccess" + AzurePLSErrorReason string = "AzureError" +) + +// AzureSubnetResourceID is a full Azure resource ID for a subnet. +// The expected format is: +// +// /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=355 +// +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/virtualNetworks/[^/]+/subnets/[^/]+$')",message="must be a valid Azure subnet resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet})" +type AzureSubnetResourceID string + +// AzureVNetResourceID is a full Azure resource ID for a virtual network. +// The expected format is: +// +// /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName} +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=355 +// +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/virtualNetworks/[^/]+$')",message="must be a valid Azure VNet resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnet})" +type AzureVNetResourceID string + +// AzureSubscriptionID is an Azure subscription ID in UUID format. +// Must be exactly 36 characters consisting of hexadecimal digits [0-9a-fA-F] and hyphens +// in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (e.g., "550e8400-e29b-41d4-a716-446655440000"). +// +// +kubebuilder:validation:MinLength=36 +// +kubebuilder:validation:MaxLength=36 +// +kubebuilder:validation:XValidation:rule="self.matches('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')",message="must be a valid UUID (e.g., 550e8400-e29b-41d4-a716-446655440000)" +type AzureSubscriptionID string + +// AzurePrivateLinkServiceSpec defines the desired state of AzurePrivateLinkService +type AzurePrivateLinkServiceSpec struct { + // loadBalancerIP is the frontend IP address of the internal load balancer that + // fronts the hosted control plane's API server. This field is populated automatically + // by the control plane operator from the kube-apiserver service status. + // It is not set by users directly. + // When set, the value must be a valid IPv4 or IPv6 address. + // + // +optional + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="loadBalancerIP must be a valid IPv4 or IPv6 address" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=39 + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // subscriptionID is the Azure subscription ID where the Private Link Service + // resources will be created. Must be a valid UUID consisting of hexadecimal + // characters and hyphens in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + // where x is a hexadecimal digit [0-9a-f] (e.g., "550e8400-e29b-41d4-a716-446655440000"). + // + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="subscriptionID is immutable once set" + SubscriptionID AzureSubscriptionID `json:"subscriptionID,omitempty"` + + // resourceGroupName is the name of the Azure resource group where the Private Link + // Service resources will be created. Must be 1-90 characters consisting of + // alphanumerics, underscores, hyphens, periods, and parentheses. Cannot end with a period. + // See https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules + // + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceGroupName is immutable once set" + // +kubebuilder:validation:XValidation:rule="self.matches(r'^[-a-zA-Z0-9_.()]+$') && !self.endsWith('.')",message="resourceGroupName must contain only alphanumerics, underscores, hyphens, periods, and parentheses, and cannot end with a period" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=90 + ResourceGroupName string `json:"resourceGroupName,omitempty"` + + // location is the Azure region where the Private Link Service resources will be + // created (e.g., "eastus", "westeurope", "centralus"). Must match the region + // of the management cluster. + // + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="location is immutable once set" + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]+$')",message="location must contain only lowercase alphanumeric characters (e.g., eastus, westeurope, centralus)" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=50 + Location string `json:"location,omitempty"` + + // natSubnetID is the full Azure resource ID of the subnet used for Private Link Service + // NAT IP allocation. This subnet must have privateLinkServiceNetworkPolicies disabled. + // If not provided, the controller will auto-create a NAT subnet in the HC's VNet. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} + // + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="natSubnetID is immutable once set" + NATSubnetID AzureSubnetResourceID `json:"natSubnetID,omitempty"` + + // additionalAllowedSubscriptions is an optional list of additional Azure subscription IDs + // permitted to create Private Endpoints to the Private Link Service. The guest cluster's + // own subscription (derived from guestSubnetID) is always automatically allowed, so it + // does not need to be listed here. + // Each entry must be a valid UUID of exactly 36 characters consisting of + // lowercase hexadecimal characters and hyphens in the format + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx where x is a hexadecimal digit [0-9a-f] + // (e.g., "550e8400-e29b-41d4-a716-446655440000"). + // A maximum of 50 subscriptions may be specified. + // + // +optional + // +listType=set + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=50 + AdditionalAllowedSubscriptions []AzureSubscriptionID `json:"additionalAllowedSubscriptions,omitempty"` + + // guestSubnetID is the full Azure resource ID of the subnet in the guest VNet where + // the Private Endpoint will be created. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} + // + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="guestSubnetID is immutable once set" + GuestSubnetID AzureSubnetResourceID `json:"guestSubnetID,omitempty"` + + // guestVNetID is the full Azure resource ID of the guest VNet for Private DNS zone linking. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnetName} + // + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="guestVNetID is immutable once set" + GuestVNetID AzureVNetResourceID `json:"guestVNetID,omitempty"` + + // baseDomain is the cluster's base domain (e.g., "example.hypershift.azure.devcluster.openshift.com"). + // Used to create a Private DNS Zone so that worker VMs can resolve the API and OAuth + // hostnames (api-., oauth-.) to the Private Endpoint IP. + // Persisted in spec so that deletion does not depend on the HostedControlPlane still existing. + // baseDomain must be at most 253 characters in length and must consist only of + // lowercase alphanumeric characters, hyphens, and periods. Each period-separated segment + // must start and end with an alphanumeric character. + // + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="baseDomain is immutable once set" + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$')",message="baseDomain must be a valid DNS domain name consisting of alphanumeric characters, hyphens, and periods, where each segment starts and ends with an alphanumeric character" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + BaseDomain string `json:"baseDomain,omitempty"` +} + +// AzurePrivateLinkServiceStatus defines the observed state of AzurePrivateLinkService +// +kubebuilder:validation:MinProperties=1 +type AzurePrivateLinkServiceStatus struct { + // conditions represent the current state of PLS infrastructure. + // Current condition types are: "AzurePrivateLinkServiceAvailable", "AzureInternalLoadBalancerAvailable", + // "AzurePLSCreated", "AzurePrivateEndpointAvailable", "AzurePrivateDNSAvailable" + // + // +optional + // +listType=map + // +listMapKey=type + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // internalLoadBalancerID is the Azure resource ID of the internal load balancer + // fronting the hosted control plane. The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/loadBalancers/{loadBalancerName} + // where subscriptionID is a UUID, resourceGroup is up to 90 characters, and + // loadBalancerName is up to 80 characters. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=284 + // +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/loadBalancers/[^/]+$')",message="internalLoadBalancerID must be an Azure load balancer resource ID in the format /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/loadBalancers/{loadBalancerName}" + InternalLoadBalancerID string `json:"internalLoadBalancerID,omitempty"` + + // privateLinkServiceID is the Azure resource ID of the Private Link Service. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/privateLinkServices/{plsName} + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=270 + // +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/privateLinkServices/[^/]+$')",message="privateLinkServiceID must be an Azure Private Link Service resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateLinkServices/{plsName})" + PrivateLinkServiceID string `json:"privateLinkServiceID,omitempty"` + + // privateLinkServiceAlias is the globally unique alias for the Private Link Service, + // auto-generated by Azure in the format {plsName}.{guid}.{region}.azure.privatelinkservice. + // MaxLength=170 covers: PLS name (80) + GUID (36) + region (19, e.g. "southcentralusstage") + // + "azure.privatelinkservice" (24) + 4 dots + 7 chars headroom. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=170 + // +kubebuilder:validation:XValidation:rule="self.matches('^[^.]+\\\\.[0-9a-fA-F-]+\\\\.[a-z0-9]+\\\\.azure\\\\.privatelinkservice$')",message="privateLinkServiceAlias must match the Azure PLS alias format {plsName}.{guid}.{region}.azure.privatelinkservice" + PrivateLinkServiceAlias string `json:"privateLinkServiceAlias,omitempty"` + + // privateEndpointID is the Azure resource ID of the Private Endpoint. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/privateEndpoints/{endpointName} + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=267 + // +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/privateEndpoints/[^/]+$')",message="privateEndpointID must be an Azure Private Endpoint resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateEndpoints/{endpointName})" + PrivateEndpointID string `json:"privateEndpointID,omitempty"` + + // privateEndpointIP is the private IP address assigned to the Private Endpoint. + // Must be a valid IPv4 (e.g., "10.0.1.4") or IPv6 address. + // + // +optional + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="privateEndpointIP must be a valid IPv4 or IPv6 address" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=39 + PrivateEndpointIP string `json:"privateEndpointIP,omitempty"` + + // privateDNSZoneID is the Azure resource ID of the Private DNS Zone. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/privateDnsZones/{zoneName} + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=265 + // +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/privateDnsZones/[^/]+$')",message="privateDNSZoneID must be an Azure Private DNS Zone resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zoneName})" + PrivateDNSZoneID string `json:"privateDNSZoneID,omitempty"` + + // dnsZoneName is the Private DNS zone name (derived from the KAS hostname). + // Persisted at creation time so that deletion does not depend on the + // HostedControlPlane still existing. + // Must be a valid DNS domain name consisting of alphanumeric characters, hyphens, + // and periods, where each segment starts and ends with an alphanumeric character + // (e.g., "api-mycluster.example.hypershift.azure.devcluster.openshift.com"). + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$')",message="dnsZoneName must be a valid DNS domain name consisting of alphanumeric characters, hyphens, and periods, where each segment starts and ends with an alphanumeric character" + DNSZoneName string `json:"dnsZoneName,omitempty"` + + // baseDomainDNSZoneID is the Azure resource ID of the base domain Private DNS Zone. + // The expected format is: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/privateDnsZones/{zoneName} + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=265 + // +kubebuilder:validation:XValidation:rule="self.matches('^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\\\.Network/privateDnsZones/[^/]+$')",message="baseDomainDNSZoneID must be an Azure Private DNS Zone resource ID (e.g., /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zoneName})" + BaseDomainDNSZoneID string `json:"baseDomainDNSZoneID,omitempty"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=azureprivatelinkservices,scope=Namespaced,shortName=azpls +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="PLS Alias",type="string",JSONPath=".status.privateLinkServiceAlias",description="Globally unique PLS alias" +// +kubebuilder:printcolumn:name="Endpoint IP",type="string",JSONPath=".status.privateEndpointIP",description="IP address of the Private Endpoint" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"AzurePrivateLinkServiceAvailable\")].status",description="PLS availability status" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// AzurePrivateLinkService represents Azure Private Link Service infrastructure +// for private connectivity to hosted cluster API servers. +type AzurePrivateLinkService struct { + metav1.TypeMeta `json:",inline"` + // metadata is the metadata for the AzurePrivateLinkService. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // spec is the specification for the AzurePrivateLinkService. + // +required + Spec AzurePrivateLinkServiceSpec `json:"spec,omitzero"` + // status is the status of the AzurePrivateLinkService. + // +optional + Status AzurePrivateLinkServiceStatus `json:"status,omitzero"` +} + +// AzurePrivateLinkServiceList contains a list of AzurePrivateLinkService +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type AzurePrivateLinkServiceList struct { + metav1.TypeMeta `json:",inline"` + // metadata is standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + // items is a list of AzurePrivateLinkService. + // +kubebuilder:validation:MaxItems=100 + // +required + Items []AzurePrivateLinkService `json:"items"` +} diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/controlplaneversion_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/controlplaneversion_types.go new file mode 100644 index 000000000..c2770e1b3 --- /dev/null +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/controlplaneversion_types.go @@ -0,0 +1,70 @@ +package v1beta1 + +import ( + configv1 "github.com/openshift/api/config/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ControlPlaneVersionStatus tracks the rollout state of management-side control plane components. +// It records the desired release, a pruned history of version transitions (newest first), and +// the last observed generation of the HostedControlPlane spec. +// +k8s:deepcopy-gen=true +type ControlPlaneVersionStatus struct { + // desired is the release version that the control plane is reconciling towards. + // It is derived from the HostedControlPlane release image fields. + // +required + Desired configv1.Release `json:"desired,omitempty,omitzero"` + + // history contains a list of versions applied to management-side control plane components. The newest entry is + // first in the list. Entries have state Completed when all ControlPlaneComponent resources report the target + // version with RolloutComplete=True. Entries have state Partial when the rollout is in progress or has failed. + // +optional + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=100 + History []ControlPlaneUpdateHistory `json:"history,omitempty"` + + // observedGeneration reports which generation of the HostedControlPlane spec is being synced. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=9007199254740992 + ObservedGeneration int64 `json:"observedGeneration,omitempty,omitzero"` +} + +// ControlPlaneUpdateHistory is a record of a single version transition for management-side +// control plane components. Each entry captures the target version, its release image, when +// the rollout started, and when (or whether) it completed. +// +k8s:deepcopy-gen=true +type ControlPlaneUpdateHistory struct { + // state reflects whether the update was fully applied. The Partial state + // indicates the update is not fully applied, while the Completed state + // indicates the update was successfully rolled out. + // +required + // +kubebuilder:validation:Enum=Completed;Partial + State configv1.UpdateState `json:"state,omitempty"` + + // startedTime is the time at which the update was started. + // +required + StartedTime metav1.Time `json:"startedTime,omitempty,omitzero"` + + // completionTime is the time at which the update completed. It is set + // when all management-side components have reached the target version. + // It is not set while the update is in progress. + // +optional + CompletionTime metav1.Time `json:"completionTime,omitempty,omitzero"` + + // version is a semantic version string identifying the update version + // (e.g. "4.20.1"). + // +required + // +kubebuilder:validation:XValidation:rule=`self.matches('^\\d+\\.\\d+\\.\\d+.*')`,message="version must start with semantic version prefix x.y.z" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + Version string `json:"version,omitempty"` + + // image is the release image pullspec used for this update. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=447 + Image string `json:"image,omitempty"` +} diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/etcdbackup_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/etcdbackup_types.go new file mode 100644 index 000000000..d59ef4752 --- /dev/null +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/etcdbackup_types.go @@ -0,0 +1,360 @@ +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + SchemeBuilder.Register(func(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &HCPEtcdBackup{}, + &HCPEtcdBackupList{}, + ) + return nil + }) +} + +// Condition types and reasons for HCPEtcdBackup. +const ( + // BackupCompleted indicates whether the etcd backup has completed. + BackupCompleted ConditionType = "BackupCompleted" + + BackupSucceededReason string = "BackupSucceeded" + BackupFailedReason string = "BackupFailed" + BackupInProgressReason string = "BackupInProgress" + BackupRejectedReason string = "BackupRejected" + EtcdUnhealthyReason string = "EtcdUnhealthy" +) + +// HCPEtcdBackupStorageType is the type of storage for etcd backups. +// +kubebuilder:validation:Enum=S3;AzureBlob +type HCPEtcdBackupStorageType string + +const ( + // S3BackupStorage indicates that the backup is stored in AWS S3. + S3BackupStorage HCPEtcdBackupStorageType = "S3" + + // AzureBlobBackupStorage indicates that the backup is stored in Azure Blob Storage. + AzureBlobBackupStorage HCPEtcdBackupStorageType = "AzureBlob" +) + +// SecretReference contains a reference to a Secret by name. +// The Secret must exist in the same namespace as the referencing resource. +type SecretReference struct { + // name is the name of the Secret. It must be a valid DNS-1123 subdomain: at most + // 253 characters, consisting of lowercase alphanumeric characters, hyphens, and periods. + // Each period-separated segment must start and end with an alphanumeric character. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')",message="name must consist only of lowercase alphanumeric characters, hyphens, and periods. Each period-separated segment must start and end with an alphanumeric character." + Name string `json:"name,omitempty"` +} + +// HCPEtcdBackupSpec defines the desired state of HCPEtcdBackup. +// HCPEtcdBackup is a one-shot backup request; the entire spec is immutable once created. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="HCPEtcdBackupSpec is immutable" +type HCPEtcdBackupSpec struct { + // storage defines the cloud storage backend where the etcd snapshot will be uploaded. + // +required + Storage HCPEtcdBackupStorage `json:"storage,omitzero"` +} + +// HCPEtcdBackupStorage defines the cloud storage backend configuration for the backup. +// Exactly one storage backend must be specified, matching the storageType discriminator. +// +union +// +kubebuilder:validation:XValidation:rule="self.storageType == 'S3' ? has(self.s3) : !has(self.s3)",message="s3 configuration is required when storageType is S3, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="self.storageType == 'AzureBlob' ? has(self.azureBlob) : !has(self.azureBlob)",message="azureBlob configuration is required when storageType is AzureBlob, and forbidden otherwise" +type HCPEtcdBackupStorage struct { + // storageType specifies the type of cloud storage backend for the etcd backup. + // Valid values are "S3" for AWS S3 storage and "AzureBlob" for Azure Blob Storage. + // +unionDiscriminator + // +required + StorageType HCPEtcdBackupStorageType `json:"storageType,omitempty"` + + // s3 specifies the S3 storage configuration for the etcd backup. + // Required when storageType is "S3", and forbidden otherwise. + // +optional + // +unionMember + S3 HCPEtcdBackupS3 `json:"s3,omitzero"` + + // azureBlob specifies the Azure Blob storage configuration for the etcd backup. + // Required when storageType is "AzureBlob", and forbidden otherwise. + // +optional + // +unionMember + AzureBlob HCPEtcdBackupAzureBlob `json:"azureBlob,omitzero"` +} + +// HCPEtcdBackupS3 defines the S3 storage configuration for etcd backups. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.kmsKeyARN) || has(self.kmsKeyARN)",message="kmsKeyARN cannot be removed once set" +type HCPEtcdBackupS3 struct { + // bucket is the name of the S3 bucket where backups are stored. + // Must be 3-63 characters, lowercase letters, numbers, hyphens, and periods only. + // Must start and end with a letter or number. Consecutive periods are not allowed. + // See https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html + // +required + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9][a-z0-9.-]*[a-z0-9]$')",message="bucket must consist of lowercase letters, numbers, hyphens, and periods, and must start and end with a letter or number" + // +kubebuilder:validation:XValidation:rule="!self.contains('..')",message="bucket must not contain consecutive periods" + Bucket string `json:"bucket,omitempty"` + + // region is the AWS region where the S3 bucket is located (e.g. "us-east-1"). + // Must be a valid AWS region identifier: lowercase letters, digits, and hyphens. + // Must start and end with an alphanumeric character, no consecutive hyphens. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z][a-z0-9-]*[a-z0-9]$')",message="region must consist of lowercase letters, digits, and hyphens, must start with a letter and end with an alphanumeric character" + // +kubebuilder:validation:XValidation:rule="!self.contains('--')",message="region must not contain consecutive hyphens" + Region string `json:"region,omitempty"` + + // keyPrefix is the S3 key prefix for the backup file. + // Must consist of safe S3 object key characters: alphanumeric characters, + // forward slashes, hyphens, underscores, periods, exclamation marks, + // asterisks, single quotes, and parentheses. + // See https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9!_.*\\'()/-]+$')",message="keyPrefix must consist of safe S3 key characters: alphanumeric characters, forward slashes, hyphens, underscores, periods, exclamation marks, asterisks, single quotes, and parentheses" + KeyPrefix string `json:"keyPrefix,omitempty"` + + // credentials references a Secret containing AWS credentials for uploading + // to S3. The Secret must exist in the Hypershift Operator namespace and contain a + // 'credentials' key with a valid AWS credentials file. + // +required + Credentials SecretReference `json:"credentials,omitzero"` + + // kmsKeyARN is the ARN of the KMS key used for server-side encryption of the backup. + // Must be a valid AWS KMS key ARN in the format + // "arn::kms:::key/" + // where partition is one of aws, aws-cn, or aws-us-gov. + // This field is immutable once set and cannot be removed. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:XValidation:rule="self.matches('^arn:(aws|aws-cn|aws-us-gov):kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$')",message="kmsKeyARN must be a valid AWS KMS key ARN (arn::kms:::key/)" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="kmsKeyARN is immutable" + KMSKeyARN string `json:"kmsKeyARN,omitempty"` +} + +// HCPEtcdBackupAzureBlob defines the Azure Blob storage configuration for etcd backups. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.encryptionKeyURL) || has(self.encryptionKeyURL)",message="encryptionKeyURL cannot be removed once set" +type HCPEtcdBackupAzureBlob struct { + // container is the name of the Azure Blob container where backups are stored. + // Must be 3-63 characters, lowercase letters, numbers, and hyphens only. + // Must start and end with a letter or number. Consecutive hyphens are not allowed. + // See https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names + // +required + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]([a-z0-9-]*[a-z0-9])?$')",message="container must consist of lowercase letters, numbers, and hyphens, and must start and end with a letter or number" + // +kubebuilder:validation:XValidation:rule="!self.contains('--')",message="container must not contain consecutive hyphens" + Container string `json:"container,omitempty"` + + // storageAccount is the name of the Azure Storage Account. + // Must be 3-24 characters, lowercase letters and numbers only. + // See https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#storage-account-name + // +required + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=24 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]+$')",message="storageAccount must consist of lowercase letters and numbers only" + StorageAccount string `json:"storageAccount,omitempty"` + + // keyPrefix is the blob name prefix for the backup file. + // Must consist of valid blob name characters: alphanumeric characters, forward slashes, + // hyphens, underscores, and periods. + // See https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9/_.-]+$')",message="keyPrefix must consist of alphanumeric characters, forward slashes, hyphens, underscores, and periods" + KeyPrefix string `json:"keyPrefix,omitempty"` + + // credentials references a Secret containing Azure credentials for uploading + // to Blob Storage. The Secret must exist in the Hypershift Operator namespace. + // +required + Credentials SecretReference `json:"credentials,omitzero"` + + // encryptionKeyURL is the URL of the Azure Key Vault key used for encryption. + // Must be a valid Azure Key Vault key URL in the format + // "https://.vault.azure.net/keys/[/]". + // This field is immutable once set and cannot be removed. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=210 + // +kubebuilder:validation:XValidation:rule="self.matches('^https://[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL must be a valid Azure Key Vault HTTPS URL (https://.vault.azure.net/keys/[/])" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="encryptionKeyURL is immutable" + EncryptionKeyURL string `json:"encryptionKeyURL,omitempty"` +} + +// HCPEtcdBackupStatus defines the observed state of HCPEtcdBackup. +// +kubebuilder:validation:MinProperties=1 +type HCPEtcdBackupStatus struct { + // conditions contains details for the current state of the etcd backup. + // The following condition types are expected: + // - "BackupCompleted": indicates whether the etcd backup has completed (True=success, False=failure). + // +optional + // +listType=map + // +listMapKey=type + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // snapshotURL is the URL of the completed backup snapshot in cloud storage. + // Must be a valid URL with scheme https or s3. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + // +kubebuilder:validation:XValidation:rule="self.matches('^(https|s3)://.*')",message="snapshotURL must be a valid URL with scheme https or s3" + SnapshotURL string `json:"snapshotURL,omitempty"` + + // encryptionMetadata contains metadata about the encryption of the backup. + // When present, at least one platform-specific encryption block must be set. + // +optional + EncryptionMetadata HCPEtcdBackupEncryptionMetadata `json:"encryptionMetadata,omitzero"` +} + +// HCPEtcdBackupEncryptionMetadata contains platform-specific metadata about the +// encryption applied to the backup artifact in cloud storage. +// The presence of a platform block indicates that encryption was applied. +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:validation:MaxProperties=1 +type HCPEtcdBackupEncryptionMetadata struct { + // aws contains AWS-specific encryption metadata for the backup. + // +optional + AWS HCPEtcdBackupEncryptionMetadataAWS `json:"aws,omitzero"` + + // azure contains Azure-specific encryption metadata for the backup. + // +optional + Azure HCPEtcdBackupEncryptionMetadataAzure `json:"azure,omitzero"` +} + +// HCPEtcdBackupEncryptionMetadataAWS contains AWS-specific encryption metadata. +// The values here reflect the encryption settings from the HCPEtcdBackupConfig input. +type HCPEtcdBackupEncryptionMetadataAWS struct { + // kmsKeyARN is the ARN of the KMS key used for server-side encryption of the backup in S3. + // Must be a valid AWS KMS key ARN in the format + // "arn::kms:::key/" + // where partition is one of aws, aws-cn, or aws-us-gov. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:XValidation:rule="self.matches('^arn:(aws|aws-cn|aws-us-gov):kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$')",message="kmsKeyARN must be a valid AWS KMS key ARN (arn::kms:::key/)" + KMSKeyARN string `json:"kmsKeyARN,omitempty"` +} + +// HCPEtcdBackupEncryptionMetadataAzure contains Azure-specific encryption metadata. +// The values here reflect the encryption settings from the HCPEtcdBackupConfig input. +type HCPEtcdBackupEncryptionMetadataAzure struct { + // encryptionKeyURL is the URL of the Azure Key Vault key used for encryption of the backup. + // Must be a valid Azure Key Vault key URL in the format + // "https://.vault.azure.net/keys/[/]". + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=210 + // +kubebuilder:validation:XValidation:rule="self.matches('^https://[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL must be a valid Azure Key Vault HTTPS URL (https://.vault.azure.net/keys/[/])" + EncryptionKeyURL string `json:"encryptionKeyURL,omitempty"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=hcpetcdbackups,scope=Namespaced,shortName=hcpetcdbk +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Completed",type="string",JSONPath=".status.conditions[?(@.type==\"BackupCompleted\")].status",description="Backup completion status" +// +kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.snapshotURL",description="Snapshot URL" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +openshift:enable:FeatureGate=HCPEtcdBackup + +// HCPEtcdBackup represents a request to back up etcd for a hosted control plane. +// This resource is feature-gated behind the HCPEtcdBackup feature gate. +type HCPEtcdBackup struct { + metav1.TypeMeta `json:",inline"` + // metadata is the metadata for the HCPEtcdBackup. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // spec is the specification for the HCPEtcdBackup. + // +required + Spec HCPEtcdBackupSpec `json:"spec,omitzero"` + // status is the status of the HCPEtcdBackup. + // +optional + Status HCPEtcdBackupStatus `json:"status,omitzero"` +} + +// HCPEtcdBackupList contains a list of HCPEtcdBackup. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type HCPEtcdBackupList struct { + metav1.TypeMeta `json:",inline"` + // metadata is standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + // items is the list of HCPEtcdBackups. + // +required + Items []HCPEtcdBackup `json:"items,omitempty"` +} + +// HCPEtcdBackupConfigPlatform identifies the cloud platform for backup encryption configuration. +// +kubebuilder:validation:Enum=AWS;Azure +type HCPEtcdBackupConfigPlatform string + +const ( + // AWSBackupConfigPlatform indicates AWS KMS encryption for backup artifacts. + AWSBackupConfigPlatform HCPEtcdBackupConfigPlatform = "AWS" + + // AzureBackupConfigPlatform indicates Azure Key Vault encryption for backup artifacts. + AzureBackupConfigPlatform HCPEtcdBackupConfigPlatform = "Azure" +) + +// HCPEtcdBackupConfig defines the backup encryption configuration that is propagated +// from the HostedCluster to the HostedControlPlane via ManagedEtcdSpec. +// Exactly one platform-specific block must be specified, matching the platform discriminator. +// +union +// +kubebuilder:validation:XValidation:rule="self.platform == 'AWS' ? has(self.aws) : !has(self.aws)",message="aws configuration is required when platform is AWS, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="self.platform == 'Azure' ? has(self.azure) : !has(self.azure)",message="azure configuration is required when platform is Azure, and forbidden otherwise" +type HCPEtcdBackupConfig struct { + // platform specifies the cloud platform for backup encryption configuration. + // Valid values are "AWS" for AWS KMS encryption and "Azure" for Azure Key Vault encryption. + // +unionDiscriminator + // +required + Platform HCPEtcdBackupConfigPlatform `json:"platform,omitempty"` + + // aws contains AWS-specific backup encryption configuration. + // Required when platform is "AWS", and forbidden otherwise. + // +optional + // +unionMember + AWS HCPEtcdBackupConfigAWS `json:"aws,omitzero"` + + // azure contains Azure-specific backup encryption configuration. + // Required when platform is "Azure", and forbidden otherwise. + // +optional + // +unionMember + Azure HCPEtcdBackupConfigAzure `json:"azure,omitzero"` +} + +// HCPEtcdBackupConfigAWS defines AWS-specific encryption settings for etcd backups. +type HCPEtcdBackupConfigAWS struct { + // kmsKeyARN is the ARN of the AWS KMS key to use for encrypting etcd backup artifacts in S3. + // Must be a valid AWS KMS key ARN in the format + // "arn::kms:::key/" + // where partition is one of aws, aws-cn, or aws-us-gov. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + // +kubebuilder:validation:XValidation:rule="self.matches('^arn:(aws|aws-cn|aws-us-gov):kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$')",message="kmsKeyARN must be a valid AWS KMS key ARN (arn::kms:::key/)" + KMSKeyARN string `json:"kmsKeyARN,omitempty"` +} + +// HCPEtcdBackupConfigAzure defines Azure-specific encryption settings for etcd backups. +type HCPEtcdBackupConfigAzure struct { + // encryptionKeyURL is the URL of the Azure Key Vault key to use for encrypting etcd backup artifacts. + // Must be a valid Azure Key Vault key URL in the format + // "https://.vault.azure.net/keys/[/]". + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=210 + // +kubebuilder:validation:XValidation:rule="self.matches('^https://[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL must be a valid Azure Key Vault HTTPS URL (https://.vault.azure.net/keys/[/])" + EncryptionKeyURL string `json:"encryptionKeyURL,omitempty"` +} diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go index bc651e453..e4ec6cac9 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go @@ -11,10 +11,7 @@ type GCPResourceReference struct { // See https://cloud.google.com/compute/docs/naming-resources for details. // // +required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - Name string `json:"name"` + Name GCPResourceName `json:"name,omitempty"` } // GCPResourceLabel is a label to apply to GCP resources created for the cluster. @@ -33,19 +30,19 @@ type GCPResourceLabel struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([_a-z0-9-]{0,61}[a-z0-9])?$` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z]([_a-z0-9-]{0,61}[a-z0-9])?$')",message="key must start with a lowercase letter, contain only lowercase letters, digits, underscores, or hyphens, and end with a letter or digit" // +kubebuilder:validation:XValidation:rule="!self.startsWith('goog')",message="Label keys starting with the reserved 'goog' prefix are not allowed" - Key string `json:"key"` + Key string `json:"key,omitempty"` // value is the value part of the label. A label value can have a maximum of 63 characters. // Empty values are allowed by GCP. If non-empty, it must start with a lowercase letter, // contain only lowercase letters, digits, underscores, or hyphens, and end with a lowercase letter or digit. // See https://cloud.google.com/compute/docs/labeling-resources for Compute Engine label requirements. // - // +optional + // +required // +kubebuilder:validation:MinLength=0 // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^$|^[a-z]([_a-z0-9-]{0,61}[a-z0-9])?$` + // +kubebuilder:validation:XValidation:rule="self.matches('^$|^[a-z]([_a-z0-9-]{0,61}[a-z0-9])?$')",message="value must be empty or start with a lowercase letter, contain only lowercase letters, digits, underscores, or hyphens, and end with a letter or digit" Value *string `json:"value,omitempty"` } @@ -93,13 +90,13 @@ type GCPNetworkConfig struct { // +required // +immutable // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Network is immutable" - Network GCPResourceReference `json:"network"` + Network GCPResourceReference `json:"network,omitzero"` // privateServiceConnectSubnet is the subnet for Private Service Connect endpoints // +required // +immutable // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Private Service Connect subnet is immutable" - PrivateServiceConnectSubnet GCPResourceReference `json:"privateServiceConnectSubnet"` + PrivateServiceConnectSubnet GCPResourceReference `json:"privateServiceConnectSubnet,omitzero"` } // GCPPlatformSpec specifies configuration for clusters running on Google Cloud Platform. @@ -115,40 +112,37 @@ type GCPPlatformSpec struct { // characters: Only lowercase letters (`a-z`), digits (`0-9`), and hyphens (`-`) are allowed // start and end: Must begin with a lowercase letter and must not end with a hyphen // valid examples: "my-project", "my-project-1", "my-project-123". + // See https://cloud.google.com/resource-manager/docs/creating-managing-projects for project ID naming rules. // // +required // +immutable // +kubebuilder:validation:MinLength=6 // +kubebuilder:validation:MaxLength=30 - // +kubebuilder:validation:Pattern=`^[a-z]([a-z0-9-]{4,28}[a-z0-9])$` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z][a-z0-9-]{4,28}[a-z0-9]$')",message="project must start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Project is immutable" - Project string `json:"project"` - - // region is the GCP region in which the cluster resides. - // Must be in the form of - (e.g., us-central1, europe-west12). - // Must contain exactly one hyphen separating the geographic area from the location. - // Must end with one or more digits. - // Valid examples: "us-central1", "europe-west2", "europe-west12", "northamerica-northeast1" - // Invalid examples: "us1" (no hyphen), "us-central" (no trailing digits), "us-central1-a" (zone suffix) + Project string `json:"project,omitempty"` + + // region is the GCP region in which the cluster resides (e.g., us-central1, europe-west2). + // Must start with lowercase letters, contain exactly one hyphen, and end with digits. // For a full list of valid regions, see: https://cloud.google.com/compute/docs/regions-zones. // // +required // +immutable // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]+-[a-z]+[0-9]+$` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z]+-[a-z]+[0-9]+$')",message="region must be a valid GCP region (e.g., us-central1, europe-west2)" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Region is immutable" - Region string `json:"region"` + Region string `json:"region,omitempty"` // networkConfig specifies VPC configuration for Private Service Connect. // Required for VPC configuration in Private Service Connect deployments. // +required - NetworkConfig GCPNetworkConfig `json:"networkConfig"` + NetworkConfig GCPNetworkConfig `json:"networkConfig,omitzero"` // endpointAccess controls API endpoint accessibility for the HostedControlPlane on GCP. // Allowed values: "Private", "PublicAndPrivate". Defaults to "Private". // +kubebuilder:validation:Enum=PublicAndPrivate;Private - // +kubebuilder:default=Private + // +default="Private" // +optional EndpointAccess GCPEndpointAccessType `json:"endpointAccess,omitempty"` @@ -161,6 +155,7 @@ type GCPPlatformSpec struct { // +optional // +listType=map // +listMapKey=key + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=60 ResourceLabels []GCPResourceLabel `json:"resourceLabels,omitempty"` @@ -189,15 +184,16 @@ type GCPWorkloadIdentityConfig struct { // projectNumber is the numeric GCP project identifier for WIF configuration. // This differs from the project ID and is required for workload identity pools. // Must be a numeric string representing the GCP project number. + // See https://cloud.google.com/resource-manager/docs/creating-managing-projects for project number details. // // This is a user-provided value obtained from GCP (found in GCP Console or via `gcloud projects describe PROJECT_ID`). // Also available in the output of `hypershift infra create gcp`. // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[0-9]+$` // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=25 + // +kubebuilder:validation:XValidation:rule="self.matches('^[0-9]+$')",message="projectNumber must contain only digits" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Project number is immutable" ProjectNumber string `json:"projectNumber,omitempty"` @@ -207,6 +203,7 @@ type GCPWorkloadIdentityConfig struct { // Allowed characters: lowercase letters (a-z), digits (0-9), hyphens (-). // Cannot start or end with a hyphen. // The prefix "gcp-" is reserved by Google and cannot be used. + // See https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers for naming rules. // // This is a user-provided value referencing a pre-created Workload Identity Pool. // Typically obtained from the output of `hypershift infra create gcp` which creates @@ -216,7 +213,7 @@ type GCPWorkloadIdentityConfig struct { // +immutable // +kubebuilder:validation:MinLength=4 // +kubebuilder:validation:MaxLength=32 - // +kubebuilder:validation:Pattern=`^[a-z]([a-z0-9-]{2,30}[a-z0-9])$` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z][a-z0-9-]{2,30}[a-z0-9]$')",message="poolID must start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit" // +kubebuilder:validation:XValidation:rule="!self.startsWith('gcp-')", message="Pool ID cannot start with reserved prefix 'gcp-'" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Pool ID is immutable" PoolID string `json:"poolID,omitempty"` @@ -227,6 +224,7 @@ type GCPWorkloadIdentityConfig struct { // Allowed characters: lowercase letters (a-z), digits (0-9), hyphens (-). // Cannot start or end with a hyphen. // The prefix "gcp-" is reserved by Google and cannot be used. + // See https://cloud.google.com/iam/docs/manage-workload-identity-pools-providers for naming rules. // // This is a user-provided value referencing a pre-created OIDC Provider within the WIF Pool. // Typically obtained from the output of `hypershift infra create gcp`. @@ -235,7 +233,7 @@ type GCPWorkloadIdentityConfig struct { // +immutable // +kubebuilder:validation:MinLength=4 // +kubebuilder:validation:MaxLength=32 - // +kubebuilder:validation:Pattern=`^[a-z]([a-z0-9-]{2,30}[a-z0-9])$` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z][a-z0-9-]{2,30}[a-z0-9]$')",message="providerID must start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and end with a letter or digit" // +kubebuilder:validation:XValidation:rule="!self.startsWith('gcp-')", message="Provider ID cannot start with reserved prefix 'gcp-'" // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Provider ID is immutable" ProviderID string `json:"providerID,omitempty"` @@ -249,6 +247,25 @@ type GCPWorkloadIdentityConfig struct { ServiceAccountsEmails GCPServiceAccountsEmails `json:"serviceAccountsEmails,omitzero"` } +// GCPServiceAccountEmail is the email address of a Google Service Account. +// Format: service-account-name@project-id.iam.gserviceaccount.com +// See https://cloud.google.com/iam/docs/service-accounts-create for service account naming rules. +// +// +kubebuilder:validation:MinLength=37 +// +kubebuilder:validation:MaxLength=85 +// +kubebuilder:validation:XValidation:rule="self.matches('^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\\\\.iam\\\\.gserviceaccount\\\\.com$')",message="email must be a valid GCP service account email (format: name@project.iam.gserviceaccount.com)" +type GCPServiceAccountEmail string + +// GCPResourceName is the name of a GCP resource following RFC 1035 naming conventions. +// Must start with a lowercase letter, contain only lowercase letters, digits, and hyphens, +// must not end with a hyphen, and be 1-63 characters long. +// See https://cloud.google.com/compute/docs/naming-resources for details. +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=63 +// +kubebuilder:validation:XValidation:rule="self.matches('^[a-z]([-a-z0-9]*[a-z0-9])?$')",message="must start with a lowercase letter, contain only lowercase letters, digits, or hyphens, and not end with a hyphen" +type GCPResourceName string + // GCPServiceAccountsEmails contains email addresses of Google Service Accounts for different controllers. // Each service account should have the appropriate IAM permissions for its specific role. type GCPServiceAccountsEmails struct { @@ -267,11 +284,8 @@ type GCPServiceAccountsEmails struct { // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - // +kubebuilder:validation:MinLength=37 - // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="NodePool is immutable" - NodePool string `json:"nodePool,omitempty"` + NodePool GCPServiceAccountEmail `json:"nodePool,omitempty"` // controlPlane is the Google Service Account email for the Control Plane Operator // that manages control plane infrastructure and resources. @@ -288,11 +302,8 @@ type GCPServiceAccountsEmails struct { // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - // +kubebuilder:validation:MinLength=37 - // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ControlPlane is immutable" - ControlPlane string `json:"controlPlane,omitempty"` + ControlPlane GCPServiceAccountEmail `json:"controlPlane,omitempty"` // cloudController is the Google Service Account email for the Cloud Controller Manager // that manages LoadBalancer services and node lifecycle in the hosted cluster. @@ -309,11 +320,8 @@ type GCPServiceAccountsEmails struct { // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - // +kubebuilder:validation:MinLength=37 - // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="CloudController is immutable" - CloudController string `json:"cloudController,omitempty"` + CloudController GCPServiceAccountEmail `json:"cloudController,omitempty"` // storage is the Google Service Account email for the GCP PD CSI Driver // that manages Persistent Disk storage operations (create, attach, delete volumes). @@ -331,11 +339,8 @@ type GCPServiceAccountsEmails struct { // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - // +kubebuilder:validation:MinLength=37 - // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Storage is immutable" - Storage string `json:"storage,omitempty"` + Storage GCPServiceAccountEmail `json:"storage,omitempty"` // imageRegistry is the Google Service Account email for the Image Registry Operator // that manages GCS storage for the internal container image registry. @@ -350,11 +355,8 @@ type GCPServiceAccountsEmails struct { // // +required // +immutable - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - // +kubebuilder:validation:MinLength=37 - // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ImageRegistry is immutable" - ImageRegistry string `json:"imageRegistry,omitempty"` + ImageRegistry GCPServiceAccountEmail `json:"imageRegistry,omitempty"` } // GCPOnHostMaintenance defines the behavior when a host maintenance event occurs. @@ -384,8 +386,8 @@ type GCPNodePoolPlatform struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=255 - // +kubebuilder:validation:Pattern=`^[a-z0-9]+(-[a-z0-9]+)*$` - MachineType string `json:"machineType"` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]+(-[a-z0-9]+)*$')",message="machineType must start and end with a lowercase letter or digit, and contain only lowercase letters, digits, and hyphens" + MachineType string `json:"machineType,omitempty"` // zone is the GCP zone where node instances will be created. // Must be a valid zone within the cluster's region. @@ -395,8 +397,8 @@ type GCPNodePoolPlatform struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]+(?:-[a-z0-9]+)*-[a-z]$` - Zone string `json:"zone"` + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z]+(?:-[a-z0-9]+)*-[a-z]$')",message="zone must be in the form of region-zone (e.g., us-central1-a)" + Zone string `json:"zone,omitempty"` // subnet is the name of the subnet where node instances will be created. // Must be a subnet within the VPC network specified in the HostedCluster's @@ -404,10 +406,7 @@ type GCPNodePoolPlatform struct { // The subnet must have enough IP addresses available for the expected number of nodes. // // +required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - Subnet string `json:"subnet"` + Subnet GCPResourceName `json:"subnet,omitempty"` // image specifies the boot image for node instances. // If unspecified, the default RHCOS image will be used based on the NodePool release payload. @@ -417,8 +416,9 @@ type GCPNodePoolPlatform struct { // - A full resource URL: https://www.googleapis.com/compute/v1/projects/rhel-cloud/global/images/rhel-8-v20231010 // // +optional + // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=2048 - Image *string `json:"image,omitempty"` + Image string `json:"image,omitempty"` // bootDisk specifies the configuration for the boot disk of node instances. // @@ -444,6 +444,7 @@ type GCPNodePoolPlatform struct { // +optional // +listType=map // +listMapKey=key + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=60 ResourceLabels []GCPResourceLabel `json:"resourceLabels,omitempty"` @@ -457,11 +458,9 @@ type GCPNodePoolPlatform struct { // // +optional // +listType=set + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=64 - // +kubebuilder:validation:items:MinLength=1 - // +kubebuilder:validation:items:MaxLength=63 - // +kubebuilder:validation:items:Pattern=`^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$` - NetworkTags []string `json:"networkTags,omitempty"` + NetworkTags []GCPResourceName `json:"networkTags,omitempty"` // provisioningModel specifies the provisioning model for node instances. // Spot and Preemptible instances cost less but can be terminated by GCP with 30 seconds notice. @@ -470,9 +469,9 @@ type GCPNodePoolPlatform struct { // If not specified, defaults to "Standard". // // +optional - // +kubebuilder:default=Standard + // +default="Standard" // +kubebuilder:validation:Enum=Standard;Spot;Preemptible - ProvisioningModel *GCPProvisioningModel `json:"provisioningModel,omitempty"` + ProvisioningModel GCPProvisioningModel `json:"provisioningModel,omitempty"` // onHostMaintenance specifies the behavior when host maintenance occurs. // For Spot and Preemptible instances, this must be "TERMINATE". @@ -481,7 +480,7 @@ type GCPNodePoolPlatform struct { // // +optional // +kubebuilder:validation:Enum=MIGRATE;TERMINATE - OnHostMaintenance *string `json:"onHostMaintenance,omitempty"` + OnHostMaintenance GCPOnHostMaintenance `json:"onHostMaintenance,omitempty"` } // GCPBootDisk specifies configuration for the boot disk of GCP node instances. @@ -490,10 +489,10 @@ type GCPBootDisk struct { // Must be at least 20 GB for RHCOS images. // // +optional - // +kubebuilder:default=64 + // +default=64 // +kubebuilder:validation:Minimum=20 // +kubebuilder:validation:Maximum=65536 - DiskSizeGB *int64 `json:"diskSizeGB,omitempty"` + DiskSizeGB int64 `json:"diskSizeGB,omitempty"` // diskType specifies the disk type for the boot disk. // Valid values include: @@ -503,15 +502,15 @@ type GCPBootDisk struct { // If not specified, defaults to "pd-balanced". // // +optional - // +kubebuilder:default="pd-balanced" + // +default="pd-balanced" // +kubebuilder:validation:Enum=pd-standard;pd-ssd;pd-balanced - DiskType *string `json:"diskType,omitempty"` + DiskType string `json:"diskType,omitempty"` // encryptionKey specifies customer-managed encryption key (CMEK) configuration. // If not specified, Google-managed encryption keys are used. // // +optional - EncryptionKey *GCPDiskEncryptionKey `json:"encryptionKey,omitempty"` + EncryptionKey GCPDiskEncryptionKey `json:"encryptionKey,omitzero"` } // GCPDiskEncryptionKey specifies configuration for customer-managed encryption keys. @@ -522,8 +521,8 @@ type GCPDiskEncryptionKey struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=2048 - // +kubebuilder:validation:Pattern=`^projects\/[a-z][a-z0-9-]{4,28}[a-z0-9]\/locations\/[a-z0-9-]+\/keyRings\/[a-zA-Z0-9_-]+\/cryptoKeys\/[a-zA-Z0-9_-]+$` - KMSKeyName string `json:"kmsKeyName"` + // +kubebuilder:validation:XValidation:rule="self.matches('^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/keyRings/[a-zA-Z0-9_-]+/cryptoKeys/[a-zA-Z0-9_-]+$')",message="kmsKeyName must be in the format projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{key}" + KMSKeyName string `json:"kmsKeyName,omitempty"` } // GCPNodeServiceAccount specifies the Google Service Account configuration for node instances. @@ -536,9 +535,7 @@ type GCPNodeServiceAccount struct { // - Storage object viewer (for pulling container images) // // +optional - // +kubebuilder:validation:MaxLength=254 - // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$` - Email *string `json:"email,omitempty"` + Email GCPServiceAccountEmail `json:"email,omitempty"` // scopes specifies the access scopes for the service account. // If not specified, defaults to standard compute scopes. @@ -550,6 +547,7 @@ type GCPNodeServiceAccount struct { // // +optional // +listType=set + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=50 // +kubebuilder:validation:items:MaxLength=512 Scopes []string `json:"scopes,omitempty"` diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcpprivateserviceconnect_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcpprivateserviceconnect_types.go index 4ff707062..7b753033d 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcpprivateserviceconnect_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcpprivateserviceconnect_types.go @@ -42,11 +42,12 @@ type DNSZoneStatus struct { // +required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 - Name string `json:"name"` + Name string `json:"name,omitempty"` // records lists the DNS records created in this zone // +optional // +listType=atomic + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 // +kubebuilder:validation:items:MaxLength=253 Records []string `json:"records,omitempty"` @@ -58,32 +59,33 @@ type GCPPrivateServiceConnectSpec struct { // Populated by the observer from service status // This value must be a valid IPv4 or IPv6 address. // +required - // +kubebuilder:validation:XValidation:rule="self.matches('^((\\\\d{1,3}\\\\.){3}\\\\d{1,3})$') || self.matches('^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$')",message="loadBalancerIP must be a valid IPv4 or IPv6 address" - // +kubebuilder:validation:MaxLength=45 - LoadBalancerIP string `json:"loadBalancerIP"` + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="loadBalancerIP must be a valid IPv4 or IPv6 address" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=39 + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` // forwardingRuleName is the name of the Internal Load Balancer forwarding rule // Populated by the reconciler via GCP API lookup // +optional - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - ForwardingRuleName string `json:"forwardingRuleName,omitempty"` + ForwardingRuleName GCPResourceName `json:"forwardingRuleName,omitempty"` - // consumerAcceptList specifies which customer projects can connect - // Accepts both project IDs (e.g. "my-project-123") and project numbers (e.g. "123456789012") + // consumerAcceptList specifies which customer projects can connect. + // Accepts both project IDs (e.g. "my-project-123") and project numbers (e.g. "123456789012"). + // A maximum of 50 entries are allowed. + // See https://cloud.google.com/resource-manager/docs/creating-managing-projects for project ID and number formats. // +required + // +listType=set // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=50 + // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:MaxLength=30 - // +kubebuilder:validation:items:Pattern=`^([a-z][a-z0-9-]{4,28}[a-z0-9]|[0-9]{6,12})$` - ConsumerAcceptList []string `json:"consumerAcceptList"` + // +kubebuilder:validation:items:XValidation:rule="self.matches('^([a-z][a-z0-9-]{4,28}[a-z0-9]|[0-9]{1,30})$')",message="each entry must be a valid GCP project ID or project number" + ConsumerAcceptList []string `json:"consumerAcceptList,omitempty"` // natSubnet is the subnet used for NAT by the Service Attachment // Auto-populated by the HyperShift Operator // +optional - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` - NATSubnet string `json:"natSubnet,omitempty"` + NATSubnet GCPResourceName `json:"natSubnet,omitempty"` } // GCPPrivateServiceConnectStatus defines the observed state of PSC infrastructure @@ -93,8 +95,7 @@ type GCPPrivateServiceConnectStatus struct { // +optional // +listType=map // +listMapKey=type - // +patchMergeKey=type - // +patchStrategy=merge + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -105,11 +106,12 @@ type GCPPrivateServiceConnectStatus struct { // +kubebuilder:validation:MaxLength=63 ServiceAttachmentName string `json:"serviceAttachmentName,omitempty"` - // serviceAttachmentURI is the URI customers use to connect + // serviceAttachmentURI is the URI customers use to connect. // Format: projects/{project}/regions/{region}/serviceAttachments/{name} + // See https://cloud.google.com/vpc/docs/configure-private-service-connect-producer for service attachment details. // +optional // +kubebuilder:validation:MaxLength=2048 - // +kubebuilder:validation:Pattern=`^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/regions/[a-z]+-[a-z0-9]+[0-9]/serviceAttachments/[a-z]([-a-z0-9]*[a-z0-9])?$` + // +kubebuilder:validation:XValidation:rule="self.matches('^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/regions/[a-z]+-[a-z]+[0-9]+/serviceAttachments/[a-z]([-a-z0-9]*[a-z0-9])?$')",message="serviceAttachmentURI must be in the format projects/{project}/regions/{region}/serviceAttachments/{name}" ServiceAttachmentURI string `json:"serviceAttachmentURI,omitempty"` // Customer Side Status (PSC Endpoint and DNS) @@ -117,12 +119,14 @@ type GCPPrivateServiceConnectStatus struct { // endpointIP is the reserved IP address for the PSC endpoint // This value must be a valid IPv4 or IPv6 address. // +optional - // +kubebuilder:validation:XValidation:rule="self == '' || self.matches('^((\\\\d{1,3}\\\\.){3}\\\\d{1,3})$') || self.matches('^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$')",message="endpointIP must be a valid IPv4 or IPv6 address" - // +kubebuilder:validation:MaxLength=45 + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="endpointIP must be a valid IPv4 or IPv6 address" + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=39 EndpointIP string `json:"endpointIP,omitempty"` // dnsZones contains DNS zone information created for this cluster // +listType=atomic + // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=5 DNSZones []DNSZoneStatus `json:"dnsZones,omitempty"` } diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go index 99cf08ba4..585a3b3d4 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go @@ -353,6 +353,12 @@ type HostedControlPlaneStatus struct { // +kubebuilder:validation:MaxLength=255 OAuthCallbackURLTemplate string `json:"oauthCallbackURLTemplate,omitempty"` + // controlPlaneVersion tracks the rollout status of the control plane + // components running on the management cluster, independently from + // the data-plane version reported in the version field. + // +optional + ControlPlaneVersion ControlPlaneVersionStatus `json:"controlPlaneVersion,omitzero"` + // versionStatus is the status of the release version applied by the // hosted control plane operator. // +optional @@ -407,6 +413,11 @@ type HostedControlPlaneStatus struct { // +optional NodeCount *int `json:"nodeCount,omitempty"` + // autoNode contains the observed state of the autoNode (Karpenter) provisioner. + // +openshift:enable:FeatureGate=AutoNodeKarpenter + // +optional + AutoNode AutoNodeStatus `json:"autoNode,omitzero"` + // configuration contains the cluster configuration status of the HostedCluster // +optional Configuration *ConfigurationStatus `json:"configuration,omitempty"` diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go index a3b49fbf9..59e13f4cb 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go @@ -195,6 +195,11 @@ const ( // recovery job was triggered. EtcdRecoveryActive ConditionType = "EtcdRecoveryActive" + // EtcdBackupSucceeded bubbles up from HCP. It indicates the result of the + // most recent etcd backup. True means the last backup completed successfully; + // False means a backup is in progress or the last backup failed. + EtcdBackupSucceeded ConditionType = "EtcdBackupSucceeded" + // ClusterSizeComputed indicates that a t-shirt size was computed for this HostedCluster. // The last transition time for this condition is used to manage how quickly transitions occur. ClusterSizeComputed = "ClusterSizeComputed" @@ -232,6 +237,12 @@ const ( // **Unknown** means the status cannot be determined due to true inability to inspect (e.g., no worker nodes exist or inspection cannot be performed), // not due to missing required components. ControlPlaneConnectionAvailable ConditionType = "ControlPlaneConnectionAvailable" + + // AutoNodeEnabled indicates whether AutoNode is configured and operational for this HostedCluster. + // **True** means AutoNode is configured in the HostedCluster spec and the Karpenter components are fully rolled out and ready. + // **False / AutoNodeProgressing** means AutoNode is being enabled or disabled — the operation is in progress. + // **False / AutoNodeNotConfigured** means AutoNode is not configured in the spec and all Karpenter components have been removed. + AutoNodeEnabled ConditionType = "AutoNodeEnabled" ) // Reasons. @@ -307,6 +318,10 @@ const ( ControlPlaneConnectionNoWorkerNodesAvailableReason = "NoWorkerNodesAvailable" ControlPlaneComponentsNotAvailable = "ComponentsNotAvailable" + + AutoNodeNotConfiguredReason = "AutoNodeNotConfigured" + AutoNodeProgressingReason = "AutoNodeProgressing" + AutoNodeEvaluationFailedReason = "AutoNodeEvaluationFailed" ) // Messages. diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go index 92e1852fb..d99f765f0 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go @@ -519,7 +519,8 @@ type Capabilities struct { // +kubebuilder:validation:XValidation:rule="self.platform.type == 'IBMCloud' ? size(self.services) >= 3 : size(self.services) >= 4",message="spec.services in body should have at least 4 items or 3 for IBMCloud" // +kubebuilder:validation:XValidation:rule=`self.platform.type != "IBMCloud" ? self.services == oldSelf.services : true`, message="Services is immutable. Changes might result in unpredictable and disruptive behavior." -// +kubebuilder:validation:XValidation:rule=`self.platform.type == "Azure" ? self.services.exists(s, s.service == "OAuthServer" && s.servicePublishingStrategy.type == "Route") : true`,message="Azure platform requires OAuthServer to use Route service publishing strategy" +// +kubebuilder:validation:XValidation:rule=`self.platform.type != "Azure" || self.platform.?azure.azureAuthenticationConfig.azureAuthenticationConfigType.orValue("") == "WorkloadIdentities" || self.services.exists(s, s.service == "OAuthServer" && s.servicePublishingStrategy.type == "Route")`,message="Azure managed platform (ARO HCP) requires OAuthServer to use Route" +// +kubebuilder:validation:XValidation:rule=`self.platform.type != "Azure" || self.platform.?azure.azureAuthenticationConfig.azureAuthenticationConfigType.orValue("") != "WorkloadIdentities" || self.services.exists(s, s.service == "OAuthServer" && (s.servicePublishingStrategy.type == "Route" || s.servicePublishingStrategy.type == "LoadBalancer"))`,message="Self-managed Azure requires OAuthServer to use Route or LoadBalancer" // +kubebuilder:validation:XValidation:rule=`self.platform.type == "Azure" ? self.services.exists(s, s.service == "Konnectivity" && s.servicePublishingStrategy.type == "Route") : true`,message="Azure platform requires Konnectivity to use Route service publishing strategy" // +kubebuilder:validation:XValidation:rule=`self.platform.type == "Azure" ? self.services.exists(s, s.service == "Ignition" && s.servicePublishingStrategy.type == "Route") : true`,message="Azure platform requires Ignition to use Route service publishing strategy" // +kubebuilder:validation:XValidation:rule=`has(self.issuerURL) || !has(self.serviceAccountSigningKey)`,message="If serviceAccountSigningKey is set, issuerURL must be set" @@ -1046,9 +1047,17 @@ type DNSSpec struct { // publicZoneID is the Hosted Zone ID where all the DNS records that are publicly accessible to the internet exist. // This field is optional and mainly leveraged in cloud environments where the DNS records for the .baseDomain are created by controllers in this zone. // Once set, this value is immutable. + // + // On Azure, this is a full Azure resource ID for a DNS Zone in the format: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/dnsZones/{zoneName} + // The maximum length of 258 is derived from Azure resource naming limits + // (see https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules): + // /subscriptions/ (15) + UUID (36) + /resourceGroups/ (16) + resource group name (90) + // + /providers/Microsoft.Network/dnsZones/ (38) + zone name (63) = 258 + // // +optional // +kubebuilder:validation:XValidation:rule=`oldSelf == "" || self == oldSelf`, message="publicZoneID is immutable" - // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:MaxLength=258 // +kubebuilder:validation:MinLength=1 // +immutable PublicZoneID string `json:"publicZoneID,omitempty"` @@ -1056,9 +1065,17 @@ type DNSSpec struct { // privateZoneID is the Hosted Zone ID where all the DNS records that are only available internally to the cluster exist. // This field is optional and mainly leveraged in cloud environments where the DNS records for the .baseDomain are created by controllers in this zone. // Once set, this value is immutable. + // + // On Azure, this is a full Azure resource ID for a Private DNS Zone in the format: + // /subscriptions/{subscriptionID}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/privateDnsZones/{zoneName} + // The maximum length of 265 is derived from Azure resource naming limits + // (see https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules): + // /subscriptions/ (15) + UUID (36) + /resourceGroups/ (16) + resource group name (90) + // + /providers/Microsoft.Network/privateDnsZones/ (45) + zone name (63) = 265 + // // +optional // +kubebuilder:validation:XValidation:rule=`oldSelf == "" || self == oldSelf`, message="privateZoneID is immutable" - // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:MaxLength=265 // +kubebuilder:validation:MinLength=1 // +immutable PrivateZoneID string `json:"privateZoneID,omitempty"` @@ -1884,7 +1901,15 @@ type EtcdSpec struct { type ManagedEtcdSpec struct { // storage specifies how etcd data is persisted. // +required + // +kubebuilder:validation:XValidation:rule="has(self.restoreSnapshotURL) == has(oldSelf.restoreSnapshotURL)",message="restoreSnapshotURL cannot be added or removed after creation" Storage ManagedEtcdStorageSpec `json:"storage"` + + // backup defines the backup configuration for managed etcd, including + // optional KMS key settings for artifact encryption in cloud storage. + // This configuration is only used when an HCPEtcdBackup CR exists. + // +optional + // +openshift:enable:FeatureGate=HCPEtcdBackup + Backup HCPEtcdBackupConfig `json:"backup,omitzero"` } // ManagedEtcdStorageType is a storage type for an etcd cluster. @@ -1928,6 +1953,8 @@ type ManagedEtcdStorageSpec struct { // +kubebuilder:validation:MaxItems=1 // +kubebuilder:validation:items:MaxLength=1024 // +kubebuilder:validation:XValidation:rule="self.size() <= 1", message="RestoreSnapshotURL shouldn't contain more than 1 entry" + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="restoreSnapshotURL is immutable" + // +kubebuilder:validation:XValidation:rule="self.size() == 0 || self[0].matches('^(https|s3)://.*')", message="restoreSnapshotURL must be a valid URL with scheme https or s3" RestoreSnapshotURL []string `json:"restoreSnapshotURL,omitempty"` } @@ -2090,6 +2117,12 @@ type HostedClusterStatus struct { // +kubebuilder:validation:MaxItems=100 Conditions []metav1.Condition `json:"conditions,omitempty"` + // controlPlaneVersion tracks the rollout status of the control plane + // components running on the management cluster, independently from + // the data-plane version reported in the version field. + // +optional + ControlPlaneVersion ControlPlaneVersionStatus `json:"controlPlaneVersion,omitzero"` + // version is the status of the release version applied to the // HostedCluster. // +optional @@ -2140,9 +2173,40 @@ type HostedClusterStatus struct { // +optional Platform *PlatformStatus `json:"platform,omitempty"` + // autoNode contains the observed state of the autoNode (Karpenter) provisioner. + // +openshift:enable:FeatureGate=AutoNodeKarpenter + // +optional + AutoNode AutoNodeStatus `json:"autoNode,omitzero"` + // configuration contains the cluster configuration status of the HostedCluster // +optional Configuration *ConfigurationStatus `json:"configuration,omitempty"` + + // lastSuccessfulEtcdBackupURL is the cloud storage URL of the most recent + // successful etcd backup snapshot. Persisted here because HCPEtcdBackup CRs + // are ephemeral and may be deleted by retention policies. + // +openshift:enable:FeatureGate=HCPEtcdBackup + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + // +kubebuilder:validation:XValidation:rule="self.matches('^(https|s3)://.*')",message="lastSuccessfulEtcdBackupURL must be a valid URL with scheme https or s3" + LastSuccessfulEtcdBackupURL string `json:"lastSuccessfulEtcdBackupURL,omitempty"` +} + +// AutoNodeStatus contains the observed state of the AutoNode provisioner. +// +kubebuilder:validation:MinProperties=1 +type AutoNodeStatus struct { + // nodeCount is the number of nodes fully provisioned by Karpenter. + // These are node objects that exist in the cluster and carry the karpenter.sh/nodepool label. + // +kubebuilder:validation:Minimum=0 + // +optional + NodeCount *int32 `json:"nodeCount,omitempty"` + + // nodeClaimCount is the total number of NodeClaims managed by Karpenter. + // This represents what Karpenter intends to provision, whether or not the node object exists yet. + // +kubebuilder:validation:Minimum=0 + // +optional + NodeClaimCount *int32 `json:"nodeClaimCount,omitempty"` } // PlatformStatus contains platform-specific status @@ -2321,11 +2385,14 @@ type OperatorConfiguration struct { // +kubebuilder:storageversion // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version.history[?(@.state==\"Completed\")].version",description="Version" +// +kubebuilder:printcolumn:name="CP Version",type="string",JSONPath=".status.controlPlaneVersion.history[?(@.state==\"Completed\")].version",description="Control Plane Version" // +kubebuilder:printcolumn:name="KubeConfig",type="string",JSONPath=".status.kubeconfig.name",description="KubeConfig Secret" // +kubebuilder:printcolumn:name="Progress",type="string",JSONPath=".status.version.history[?(@.state!=\"\")].state",description="Progress" // +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status",description="Available" // +kubebuilder:printcolumn:name="Progressing",type="string",JSONPath=".status.conditions[?(@.type==\"Progressing\")].status",description="Progressing" // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].message",description="Message" +// +kubebuilder:printcolumn:name="CP Progress",type="string",JSONPath=".status.controlPlaneVersion.history[0].state",description="Control Plane Progress",priority=1 +// +kubebuilder:printcolumn:name="DP Progress",type="string",JSONPath=".status.version.history[0].state",description="Data Plane Progress",priority=1 type HostedCluster struct { metav1.TypeMeta `json:",inline"` // metadata is the metadata for the HostedCluster. diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/nodepool_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/nodepool_types.go index 32811c442..dc17f42ee 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/nodepool_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/nodepool_types.go @@ -25,6 +25,10 @@ const ( // IgnitionServerTokenExpirationTimestampAnnotation holds the time that a ignition token expires and should be // removed from the cluster. IgnitionServerTokenExpirationTimestampAnnotation = "hypershift.openshift.io/ignition-token-expiration-timestamp" + + // NodePoolReleaseVersionAnnotation is set on userdata Secrets (Replace strategy) and on Machines (InPlace strategy) + // to track the OCP release version associated with each machine. + NodePoolReleaseVersionAnnotation = "hypershift.openshift.io/release-version" ) // ImageType specifies the type of image to use for node instances. @@ -250,6 +254,11 @@ type NodePoolStatus struct { // +kubebuilder:validation:MaxLength=64 Version string `json:"version,omitempty"` + // nodesInfo contains aggregated information observed from nodes belonging + // to this NodePool. + // +optional + NodesInfo NodePoolNodesInfo `json:"nodesInfo,omitzero"` + // platform holds the specific statuses // +optional Platform *NodePoolPlatformStatus `json:"platform,omitempty"` @@ -261,6 +270,50 @@ type NodePoolStatus struct { Conditions []NodePoolCondition `json:"conditions,omitempty"` } +// NodePoolNodesInfo aggregates observed information about nodes belonging to this NodePool. +type NodePoolNodesInfo struct { + // nodeVersions summarizes the versions and health of nodes belonging + // to this NodePool. Each entry represents a distinct version combination + // and the number of ready/unready nodes running it. + // +kubebuilder:validation:MaxItems=100 + // +kubebuilder:validation:MinItems=1 + // +listType=atomic + // +required + NodeVersions []NodeVersion `json:"nodeVersions,omitempty"` +} + +// NodeVersion represents a version combination and the count of ready and unready nodes running it. +type NodeVersion struct { + // ocpVersion is the OpenShift release version this node was provisioned + // or upgraded with. + // +kubebuilder:validation:XValidation:rule=`self.matches('^\\d+\\.\\d+\\.\\d+.*$')`,message="ocpVersion must start with semantic version prefix x.y.z" + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:MinLength=1 + // +required + OCPVersion string `json:"ocpVersion,omitempty"` + + // kubeletVersion is the kubelet version reported by the node, as observed + // from Machine.Status.NodeInfo.KubeletVersion. + // +kubebuilder:validation:XValidation:rule=`self.matches('^v?\\d+\\.\\d+\\.\\d+.*$')`,message="kubeletVersion must start with semantic version prefix x.y.z (optional leading 'v')" + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:MinLength=1 + // +required + KubeletVersion string `json:"kubeletVersion,omitempty"` + + // readyNodeCount is the number of nodes running this version where the + // CAPI NodeHealthy condition is True. + // +kubebuilder:validation:Minimum=0 + // +required + ReadyNodeCount *int32 `json:"readyNodeCount,omitempty"` + + // unreadyNodeCount is the number of nodes running this version where the + // CAPI NodeHealthy condition is not True. Useful for tracking upgrade + // progress and detecting stuck nodes. + // +kubebuilder:validation:Minimum=0 + // +required + UnreadyNodeCount *int32 `json:"unreadyNodeCount,omitempty"` +} + // NodePoolList contains a list of NodePools. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type NodePoolList struct { diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go index c1e455682..ee74790f5 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go @@ -62,6 +62,7 @@ type ClusterNetworkOperatorSpec struct { // OVNKubernetesConfig contains OVN-Kubernetes specific configuration options. // https://github.com/openshift/api/blob/6d3c4e25a8d3aeb57ad61649d80c38cbd27d1cc8/operator/v1/types_network.go#L400-L471 // +kubebuilder:validation:XValidation:rule="!has(self.ipv4) || !has(self.ipv4.internalJoinSubnet) || !has(self.ipv4.internalTransitSwitchSubnet) || self.ipv4.internalJoinSubnet != self.ipv4.internalTransitSwitchSubnet", message="internalJoinSubnet and internalTransitSwitchSubnet must not be the same" +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.mtu) || has(self.mtu)",message="mtu is immutable once set and cannot be removed" // +kubebuilder:validation:MinProperties=1 type OVNKubernetesConfig struct { // ipv4 allows users to configure IP settings for IPv4 connections. When omitted, @@ -69,6 +70,23 @@ type OVNKubernetesConfig struct { // fields within ipv4 for details of default values. // +optional IPv4 *OVNIPv4Config `json:"ipv4,omitempty"` + + // mtu is the MTU to use for the tunnel interface on hosted cluster nodes. + // This must be 100 bytes smaller than the uplink MTU. + // When unset, the cluster-network-operator will determine the MTU automatically + // based on the infrastructure (e.g., for commercial AWS regions, it defaults + // to 8901 based on the 9001 uplink MTU minus 100 bytes overhead). + // Some non-commercial AWS regions do not support 9001 uplink MTU, + // requiring this field to be explicitly set to a lower value. + // The maximum is 9216, which is the standard jumbo frame upper limit + // supported by datacenter and cloud network interfaces. + // The minimum is 576, which is the minimum IPv4 MTU per RFC 791. + // This field is immutable once set. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="mtu is immutable once set" + // +kubebuilder:validation:Minimum=576 + // +kubebuilder:validation:Maximum=9216 + // +optional + MTU int32 `json:"mtu,omitempty"` } // OVNIPv4Config contains IPv4-specific configuration options for OVN-Kubernetes. diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go index c1d7cc029..558be0746 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -571,6 +571,31 @@ func (in *AutoNode) DeepCopy() *AutoNode { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoNodeStatus) DeepCopyInto(out *AutoNodeStatus) { + *out = *in + if in.NodeCount != nil { + in, out := &in.NodeCount, &out.NodeCount + *out = new(int32) + **out = **in + } + if in.NodeClaimCount != nil { + in, out := &in.NodeClaimCount, &out.NodeClaimCount + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoNodeStatus. +func (in *AutoNodeStatus) DeepCopy() *AutoNodeStatus { + if in == nil { + return nil + } + out := new(AutoNodeStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureAuthenticationConfiguration) DeepCopyInto(out *AzureAuthenticationConfiguration) { *out = *in @@ -694,6 +719,7 @@ func (in *AzureNodePoolPlatform) DeepCopy() *AzureNodePoolPlatform { func (in *AzurePlatformSpec) DeepCopyInto(out *AzurePlatformSpec) { *out = *in in.AzureAuthenticationConfig.DeepCopyInto(&out.AzureAuthenticationConfig) + in.Private.DeepCopyInto(&out.Private) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePlatformSpec. @@ -706,6 +732,143 @@ func (in *AzurePlatformSpec) DeepCopy() *AzurePlatformSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzurePrivateLinkService) DeepCopyInto(out *AzurePrivateLinkService) { + *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 AzurePrivateLinkService. +func (in *AzurePrivateLinkService) DeepCopy() *AzurePrivateLinkService { + if in == nil { + return nil + } + out := new(AzurePrivateLinkService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AzurePrivateLinkService) 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 *AzurePrivateLinkServiceList) DeepCopyInto(out *AzurePrivateLinkServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AzurePrivateLinkService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateLinkServiceList. +func (in *AzurePrivateLinkServiceList) DeepCopy() *AzurePrivateLinkServiceList { + if in == nil { + return nil + } + out := new(AzurePrivateLinkServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AzurePrivateLinkServiceList) 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 *AzurePrivateLinkServiceSpec) DeepCopyInto(out *AzurePrivateLinkServiceSpec) { + *out = *in + if in.AdditionalAllowedSubscriptions != nil { + in, out := &in.AdditionalAllowedSubscriptions, &out.AdditionalAllowedSubscriptions + *out = make([]AzureSubscriptionID, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateLinkServiceSpec. +func (in *AzurePrivateLinkServiceSpec) DeepCopy() *AzurePrivateLinkServiceSpec { + if in == nil { + return nil + } + out := new(AzurePrivateLinkServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzurePrivateLinkServiceStatus) DeepCopyInto(out *AzurePrivateLinkServiceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateLinkServiceStatus. +func (in *AzurePrivateLinkServiceStatus) DeepCopy() *AzurePrivateLinkServiceStatus { + if in == nil { + return nil + } + out := new(AzurePrivateLinkServiceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzurePrivateLinkSpec) DeepCopyInto(out *AzurePrivateLinkSpec) { + *out = *in + if in.AdditionalAllowedSubscriptions != nil { + in, out := &in.AdditionalAllowedSubscriptions, &out.AdditionalAllowedSubscriptions + *out = make([]AzureSubscriptionID, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateLinkSpec. +func (in *AzurePrivateLinkSpec) DeepCopy() *AzurePrivateLinkSpec { + if in == nil { + return nil + } + out := new(AzurePrivateLinkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzurePrivateSpec) DeepCopyInto(out *AzurePrivateSpec) { + *out = *in + in.PrivateLink.DeepCopyInto(&out.PrivateLink) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateSpec. +func (in *AzurePrivateSpec) DeepCopy() *AzurePrivateSpec { + if in == nil { + return nil + } + out := new(AzurePrivateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureResourceManagedIdentities) DeepCopyInto(out *AzureResourceManagedIdentities) { *out = *in @@ -758,6 +921,7 @@ func (in *AzureWorkloadIdentities) DeepCopyInto(out *AzureWorkloadIdentities) { out.NodePoolManagement = in.NodePoolManagement out.CloudProvider = in.CloudProvider out.Network = in.Network + out.ControlPlaneOperator = in.ControlPlaneOperator } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureWorkloadIdentities. @@ -1314,6 +1478,46 @@ func (in *ControlPlaneManagedIdentities) DeepCopy() *ControlPlaneManagedIdentiti return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneUpdateHistory) DeepCopyInto(out *ControlPlaneUpdateHistory) { + *out = *in + in.StartedTime.DeepCopyInto(&out.StartedTime) + in.CompletionTime.DeepCopyInto(&out.CompletionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneUpdateHistory. +func (in *ControlPlaneUpdateHistory) DeepCopy() *ControlPlaneUpdateHistory { + if in == nil { + return nil + } + out := new(ControlPlaneUpdateHistory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneVersionStatus) DeepCopyInto(out *ControlPlaneVersionStatus) { + *out = *in + in.Desired.DeepCopyInto(&out.Desired) + if in.History != nil { + in, out := &in.History, &out.History + *out = make([]ControlPlaneUpdateHistory, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneVersionStatus. +func (in *ControlPlaneVersionStatus) DeepCopy() *ControlPlaneVersionStatus { + if in == nil { + return nil + } + out := new(ControlPlaneVersionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSSpec) DeepCopyInto(out *DNSSpec) { *out = *in @@ -1488,21 +1692,7 @@ func (in *FilterByNeutronTags) DeepCopy() *FilterByNeutronTags { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPBootDisk) DeepCopyInto(out *GCPBootDisk) { *out = *in - if in.DiskSizeGB != nil { - in, out := &in.DiskSizeGB, &out.DiskSizeGB - *out = new(int64) - **out = **in - } - if in.DiskType != nil { - in, out := &in.DiskType, &out.DiskType - *out = new(string) - **out = **in - } - if in.EncryptionKey != nil { - in, out := &in.EncryptionKey, &out.EncryptionKey - *out = new(GCPDiskEncryptionKey) - **out = **in - } + out.EncryptionKey = in.EncryptionKey } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPBootDisk. @@ -1550,15 +1740,10 @@ func (in *GCPNetworkConfig) DeepCopy() *GCPNetworkConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPNodePoolPlatform) DeepCopyInto(out *GCPNodePoolPlatform) { *out = *in - if in.Image != nil { - in, out := &in.Image, &out.Image - *out = new(string) - **out = **in - } if in.BootDisk != nil { in, out := &in.BootDisk, &out.BootDisk *out = new(GCPBootDisk) - (*in).DeepCopyInto(*out) + **out = **in } if in.ServiceAccount != nil { in, out := &in.ServiceAccount, &out.ServiceAccount @@ -1574,19 +1759,9 @@ func (in *GCPNodePoolPlatform) DeepCopyInto(out *GCPNodePoolPlatform) { } if in.NetworkTags != nil { in, out := &in.NetworkTags, &out.NetworkTags - *out = make([]string, len(*in)) + *out = make([]GCPResourceName, len(*in)) copy(*out, *in) } - if in.ProvisioningModel != nil { - in, out := &in.ProvisioningModel, &out.ProvisioningModel - *out = new(GCPProvisioningModel) - **out = **in - } - if in.OnHostMaintenance != nil { - in, out := &in.OnHostMaintenance, &out.OnHostMaintenance - *out = new(string) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPNodePoolPlatform. @@ -1602,11 +1777,6 @@ func (in *GCPNodePoolPlatform) DeepCopy() *GCPNodePoolPlatform { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPNodeServiceAccount) DeepCopyInto(out *GCPNodeServiceAccount) { *out = *in - if in.Email != nil { - in, out := &in.Email, &out.Email - *out = new(string) - **out = **in - } if in.Scopes != nil { in, out := &in.Scopes, &out.Scopes *out = make([]string, len(*in)) @@ -1822,6 +1992,247 @@ func (in *GCPWorkloadIdentityConfig) DeepCopy() *GCPWorkloadIdentityConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackup) DeepCopyInto(out *HCPEtcdBackup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackup. +func (in *HCPEtcdBackup) DeepCopy() *HCPEtcdBackup { + if in == nil { + return nil + } + out := new(HCPEtcdBackup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HCPEtcdBackup) 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 *HCPEtcdBackupAzureBlob) DeepCopyInto(out *HCPEtcdBackupAzureBlob) { + *out = *in + out.Credentials = in.Credentials +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupAzureBlob. +func (in *HCPEtcdBackupAzureBlob) DeepCopy() *HCPEtcdBackupAzureBlob { + if in == nil { + return nil + } + out := new(HCPEtcdBackupAzureBlob) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupConfig) DeepCopyInto(out *HCPEtcdBackupConfig) { + *out = *in + out.AWS = in.AWS + out.Azure = in.Azure +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupConfig. +func (in *HCPEtcdBackupConfig) DeepCopy() *HCPEtcdBackupConfig { + if in == nil { + return nil + } + out := new(HCPEtcdBackupConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupConfigAWS) DeepCopyInto(out *HCPEtcdBackupConfigAWS) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupConfigAWS. +func (in *HCPEtcdBackupConfigAWS) DeepCopy() *HCPEtcdBackupConfigAWS { + if in == nil { + return nil + } + out := new(HCPEtcdBackupConfigAWS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupConfigAzure) DeepCopyInto(out *HCPEtcdBackupConfigAzure) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupConfigAzure. +func (in *HCPEtcdBackupConfigAzure) DeepCopy() *HCPEtcdBackupConfigAzure { + if in == nil { + return nil + } + out := new(HCPEtcdBackupConfigAzure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupEncryptionMetadata) DeepCopyInto(out *HCPEtcdBackupEncryptionMetadata) { + *out = *in + out.AWS = in.AWS + out.Azure = in.Azure +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupEncryptionMetadata. +func (in *HCPEtcdBackupEncryptionMetadata) DeepCopy() *HCPEtcdBackupEncryptionMetadata { + if in == nil { + return nil + } + out := new(HCPEtcdBackupEncryptionMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupEncryptionMetadataAWS) DeepCopyInto(out *HCPEtcdBackupEncryptionMetadataAWS) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupEncryptionMetadataAWS. +func (in *HCPEtcdBackupEncryptionMetadataAWS) DeepCopy() *HCPEtcdBackupEncryptionMetadataAWS { + if in == nil { + return nil + } + out := new(HCPEtcdBackupEncryptionMetadataAWS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupEncryptionMetadataAzure) DeepCopyInto(out *HCPEtcdBackupEncryptionMetadataAzure) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupEncryptionMetadataAzure. +func (in *HCPEtcdBackupEncryptionMetadataAzure) DeepCopy() *HCPEtcdBackupEncryptionMetadataAzure { + if in == nil { + return nil + } + out := new(HCPEtcdBackupEncryptionMetadataAzure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupList) DeepCopyInto(out *HCPEtcdBackupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HCPEtcdBackup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupList. +func (in *HCPEtcdBackupList) DeepCopy() *HCPEtcdBackupList { + if in == nil { + return nil + } + out := new(HCPEtcdBackupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HCPEtcdBackupList) 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 *HCPEtcdBackupS3) DeepCopyInto(out *HCPEtcdBackupS3) { + *out = *in + out.Credentials = in.Credentials +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupS3. +func (in *HCPEtcdBackupS3) DeepCopy() *HCPEtcdBackupS3 { + if in == nil { + return nil + } + out := new(HCPEtcdBackupS3) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupSpec) DeepCopyInto(out *HCPEtcdBackupSpec) { + *out = *in + out.Storage = in.Storage +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupSpec. +func (in *HCPEtcdBackupSpec) DeepCopy() *HCPEtcdBackupSpec { + if in == nil { + return nil + } + out := new(HCPEtcdBackupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupStatus) DeepCopyInto(out *HCPEtcdBackupStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.EncryptionMetadata = in.EncryptionMetadata +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupStatus. +func (in *HCPEtcdBackupStatus) DeepCopy() *HCPEtcdBackupStatus { + if in == nil { + return nil + } + out := new(HCPEtcdBackupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HCPEtcdBackupStorage) DeepCopyInto(out *HCPEtcdBackupStorage) { + *out = *in + out.S3 = in.S3 + out.AzureBlob = in.AzureBlob +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPEtcdBackupStorage. +func (in *HCPEtcdBackupStorage) DeepCopy() *HCPEtcdBackupStorage { + if in == nil { + return nil + } + out := new(HCPEtcdBackupStorage) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostedCluster) DeepCopyInto(out *HostedCluster) { *out = *in @@ -1999,6 +2410,7 @@ func (in *HostedClusterStatus) DeepCopyInto(out *HostedClusterStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.ControlPlaneVersion.DeepCopyInto(&out.ControlPlaneVersion) if in.Version != nil { in, out := &in.Version, &out.Version *out = new(ClusterVersionStatus) @@ -2025,6 +2437,7 @@ func (in *HostedClusterStatus) DeepCopyInto(out *HostedClusterStatus) { *out = new(PlatformStatus) (*in).DeepCopyInto(*out) } + in.AutoNode.DeepCopyInto(&out.AutoNode) if in.Configuration != nil { in, out := &in.Configuration, &out.Configuration *out = new(ConfigurationStatus) @@ -2229,6 +2642,7 @@ func (in *HostedControlPlaneStatus) DeepCopyInto(out *HostedControlPlaneStatus) **out = **in } out.ControlPlaneEndpoint = in.ControlPlaneEndpoint + in.ControlPlaneVersion.DeepCopyInto(&out.ControlPlaneVersion) if in.VersionStatus != nil { in, out := &in.VersionStatus, &out.VersionStatus *out = new(ClusterVersionStatus) @@ -2263,6 +2677,7 @@ func (in *HostedControlPlaneStatus) DeepCopyInto(out *HostedControlPlaneStatus) *out = new(int) **out = **in } + in.AutoNode.DeepCopyInto(&out.AutoNode) if in.Configuration != nil { in, out := &in.Configuration, &out.Configuration *out = new(ConfigurationStatus) @@ -2950,6 +3365,7 @@ func (in *ManagedAzureKeyVault) DeepCopy() *ManagedAzureKeyVault { func (in *ManagedEtcdSpec) DeepCopyInto(out *ManagedEtcdSpec) { *out = *in in.Storage.DeepCopyInto(&out.Storage) + out.Backup = in.Backup } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedEtcdSpec. @@ -3163,6 +3579,28 @@ func (in *NodePoolManagement) DeepCopy() *NodePoolManagement { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolNodesInfo) DeepCopyInto(out *NodePoolNodesInfo) { + *out = *in + if in.NodeVersions != nil { + in, out := &in.NodeVersions, &out.NodeVersions + *out = make([]NodeVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolNodesInfo. +func (in *NodePoolNodesInfo) DeepCopy() *NodePoolNodesInfo { + if in == nil { + return nil + } + out := new(NodePoolNodesInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodePoolPlatform) DeepCopyInto(out *NodePoolPlatform) { *out = *in @@ -3306,6 +3744,7 @@ func (in *NodePoolSpec) DeepCopy() *NodePoolSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodePoolStatus) DeepCopyInto(out *NodePoolStatus) { *out = *in + in.NodesInfo.DeepCopyInto(&out.NodesInfo) if in.Platform != nil { in, out := &in.Platform, &out.Platform *out = new(NodePoolPlatformStatus) @@ -3345,6 +3784,31 @@ func (in *NodePortPublishingStrategy) DeepCopy() *NodePortPublishingStrategy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeVersion) DeepCopyInto(out *NodeVersion) { + *out = *in + if in.ReadyNodeCount != nil { + in, out := &in.ReadyNodeCount, &out.ReadyNodeCount + *out = new(int32) + **out = **in + } + if in.UnreadyNodeCount != nil { + in, out := &in.UnreadyNodeCount, &out.UnreadyNodeCount + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeVersion. +func (in *NodeVersion) DeepCopy() *NodeVersion { + if in == nil { + return nil + } + out := new(NodeVersion) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OVNIPv4Config) DeepCopyInto(out *OVNIPv4Config) { *out = *in @@ -3945,6 +4409,21 @@ func (in *SecretEncryptionSpec) DeepCopy() *SecretEncryptionSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretReference) DeepCopyInto(out *SecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. +func (in *SecretReference) DeepCopy() *SecretReference { + if in == nil { + return nil + } + out := new(SecretReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceNetworkEntry) DeepCopyInto(out *ServiceNetworkEntry) { *out = *in diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml index db8ddc7b2..2da20a831 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml @@ -19,6 +19,43 @@ awsendpointservices.hypershift.openshift.io: TopLevelFeatureGates: [] Version: v1beta1 +azureprivatelinkservices.hypershift.openshift.io: + Annotations: {} + ApprovedPRNumber: "" + CRDName: azureprivatelinkservices.hypershift.openshift.io + Capability: "" + Category: "" + FeatureGates: [] + FilenameOperatorName: "" + FilenameOperatorOrdering: "" + FilenameRunLevel: "" + GroupName: hypershift.openshift.io + HasStatus: true + KindName: AzurePrivateLinkService + Labels: {} + PluralName: azureprivatelinkservices + PrinterColumns: + - description: Globally unique PLS alias + jsonPath: .status.privateLinkServiceAlias + name: PLS Alias + type: string + - description: IP address of the Private Endpoint + jsonPath: .status.privateEndpointIP + name: Endpoint IP + type: string + - description: PLS availability status + jsonPath: .status.conditions[?(@.type=="AzurePrivateLinkServiceAvailable")].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + Scope: Namespaced + ShortNames: + - azpls + TopLevelFeatureGates: [] + Version: v1beta1 + certificatesigningrequestapprovals.hypershift.openshift.io: Annotations: {} ApprovedPRNumber: "" @@ -125,6 +162,41 @@ gcpprivateserviceconnects.hypershift.openshift.io: - GCPPlatform Version: v1beta1 +hcpetcdbackups.hypershift.openshift.io: + Annotations: {} + ApprovedPRNumber: "" + CRDName: hcpetcdbackups.hypershift.openshift.io + Capability: "" + Category: "" + FeatureGates: + - HCPEtcdBackup + FilenameOperatorName: "" + FilenameOperatorOrdering: "" + FilenameRunLevel: "" + GroupName: hypershift.openshift.io + HasStatus: true + KindName: HCPEtcdBackup + Labels: {} + PluralName: hcpetcdbackups + PrinterColumns: + - description: Backup completion status + jsonPath: .status.conditions[?(@.type=="BackupCompleted")].status + name: Completed + type: string + - description: Snapshot URL + jsonPath: .status.snapshotURL + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + Scope: Namespaced + ShortNames: + - hcpetcdbk + TopLevelFeatureGates: + - HCPEtcdBackup + Version: v1beta1 + hostedclusters.hypershift.openshift.io: Annotations: {} ApprovedPRNumber: "" @@ -139,10 +211,10 @@ hostedclusters.hypershift.openshift.io: - ExternalOIDCWithUIDAndExtraClaimMappings - ExternalOIDCWithUpstreamParity - GCPPlatform + - HCPEtcdBackup - HyperShiftOnlyDynamicResourceAllocation - ImageStreamImportMode - KMSEncryptionProvider - - NetworkDiagnosticsConfig - OpenStack FilenameOperatorName: "" FilenameOperatorOrdering: "" @@ -157,6 +229,10 @@ hostedclusters.hypershift.openshift.io: jsonPath: .status.version.history[?(@.state=="Completed")].version name: Version type: string + - description: Control Plane Version + jsonPath: .status.controlPlaneVersion.history[?(@.state=="Completed")].version + name: CP Version + type: string - description: KubeConfig Secret jsonPath: .status.kubeconfig.name name: KubeConfig @@ -177,6 +253,16 @@ hostedclusters.hypershift.openshift.io: jsonPath: .status.conditions[?(@.type=="Available")].message name: Message type: string + - description: Control Plane Progress + jsonPath: .status.controlPlaneVersion.history[0].state + name: CP Progress + priority: 1 + type: string + - description: Data Plane Progress + jsonPath: .status.version.history[0].state + name: DP Progress + priority: 1 + type: string Scope: Namespaced ShortNames: - hc @@ -198,10 +284,10 @@ hostedcontrolplanes.hypershift.openshift.io: - ExternalOIDCWithUIDAndExtraClaimMappings - ExternalOIDCWithUpstreamParity - GCPPlatform + - HCPEtcdBackup - HyperShiftOnlyDynamicResourceAllocation - ImageStreamImportMode - KMSEncryptionProvider - - NetworkDiagnosticsConfig - OpenStack FilenameOperatorName: "" FilenameOperatorOrdering: "" diff --git a/vendor/modules.txt b/vendor/modules.txt index 5dc6d32ab..e8f2aa7d3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -136,7 +136,7 @@ github.com/onsi/gomega/matchers/support/goraph/edge github.com/onsi/gomega/matchers/support/goraph/node github.com/onsi/gomega/matchers/support/goraph/util github.com/onsi/gomega/types -# github.com/openshift/api v0.0.0-20260120150926-4c643a652d54 +# github.com/openshift/api v0.0.0-20260304122341-cf5d8996109f ## explicit; go 1.24.0 github.com/openshift/api/config/v1 github.com/openshift/api/operator/v1 @@ -158,7 +158,7 @@ github.com/openshift/hive/apis/hive/v1/openstack github.com/openshift/hive/apis/hive/v1/ovirt github.com/openshift/hive/apis/hive/v1/vsphere github.com/openshift/hive/apis/scheme -# github.com/openshift/hypershift/api v0.0.0-20260317154635-8eaac177f1b0 +# github.com/openshift/hypershift/api v0.0.0-20260410203959-783f7956d4f9 ## explicit; go 1.25.3 github.com/openshift/hypershift/api/hypershift/v1beta1 github.com/openshift/hypershift/api/ibmcapi From a1b2e0d19a0b2d62c07d948f90886f384676c087 Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Tue, 7 Apr 2026 17:42:02 +0200 Subject: [PATCH 2/4] feat(backup): integrate HCPEtcdBackup lifecycle into OADP backup flow Add etcdSnapshot backup method that creates and monitors HCPEtcdBackup CRs during Velero backup. When etcdBackupMethod=etcdSnapshot is configured in the plugin ConfigMap, the plugin: - Creates an HCPEtcdBackup CR in the HCP namespace using BSL storage config - Copies BSL credentials to the HO namespace (remapping key for controller) - Polls the CR until backup completes or fails - Excludes etcd pods and PVCs from Velero backup (no CSI/FS backup needed) - Stores the etcd snapshot alongside the Velero backup data in the BSL The default method remains volumeSnapshot (unchanged behavior). Also cleans up dead config parameters (readoptNodes, managedServices, awsRegenPrivateLink) and registers apiextensionsv1 in the scheme for CRD existence checks. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Juan Manuel Parrilla Madrid --- pkg/common/scheme.go | 4 + pkg/common/types.go | 22 ++ pkg/common/utils.go | 27 ++ pkg/common/utils_test.go | 28 ++ pkg/core/backup.go | 180 ++++++++- pkg/core/types/types.go | 9 +- pkg/core/validation/backup.go | 8 +- pkg/core/validation/backup_test.go | 20 +- pkg/core/validation/restore.go | 16 +- pkg/etcdbackup/orchestrator.go | 365 +++++++++++++++++++ pkg/etcdbackup/orchestrator_test.go | 544 ++++++++++++++++++++++++++++ pkg/platform/aws/aws.go | 9 - 12 files changed, 1177 insertions(+), 55 deletions(-) create mode 100644 pkg/etcdbackup/orchestrator.go create mode 100644 pkg/etcdbackup/orchestrator_test.go diff --git a/pkg/common/scheme.go b/pkg/common/scheme.go index 775125f93..ff3acfe05 100644 --- a/pkg/common/scheme.go +++ b/pkg/common/scheme.go @@ -7,6 +7,7 @@ import ( veleroapiv1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" veleroapiv2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -35,6 +36,9 @@ func init() { if err := hive.AddToScheme(CustomScheme); err != nil { errs = append(errs, err) } + if err := apiextensionsv1.AddToScheme(CustomScheme); err != nil { + errs = append(errs, err) + } if len(errs) > 0 { panic(errs) diff --git a/pkg/common/types.go b/pkg/common/types.go index f6ee67281..acdd32b57 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -16,6 +16,9 @@ const ( // Integration with Hypershift, more info here: https://github.com/openshift/hypershift/pull/6195 HostedClusterRestoredFromBackupAnnotation string = "hypershift.openshift.io/restored-from-backup" + // Etcd snapshot URL annotation: set during backup so the restore plugin can read it + // (Velero strips status from items during restore, so we persist it as an annotation) + EtcdSnapshotURLAnnotation string = "hypershift.openshift.io/etcd-snapshot-url" // hypershift/cluster-api kinds HostedClusterKind string = "HostedCluster" @@ -25,6 +28,25 @@ const ( PersistentVolumeClaimKind string = "PersistentVolumeClaim" ClusterDeploymentKind string = "ClusterDeployment" DataVolumeKind string = "DataVolume" + HCPEtcdBackupKind string = "HCPEtcdBackup" + + // Default HyperShift Operator namespace + DefaultHONamespace string = "hypershift" + // ConfigMap key to override the HO namespace + ConfigKeyHONamespace string = "hoNamespace" + + // Etcd backup method configuration + ConfigKeyEtcdBackupMethod string = "etcdBackupMethod" + EtcdBackupMethodVolume string = "volumeSnapshot" + EtcdBackupMethodEtcdSnapshot string = "etcdSnapshot" + + // Velero annotation to exclude specific volumes from backup + BackupVolumesExcludesAnnotation string = "backup.velero.io/backup-volumes-excludes" + // Etcd data volume name in the StatefulSet pod + EtcdDataVolumeName string = "data" + // Etcd PVC name prefix (StatefulSet pattern: {volumeName}-{stsName}-{index}) + EtcdPVCPrefix string = "data-etcd-" + ) var ( diff --git a/pkg/common/utils.go b/pkg/common/utils.go index 8cf79e7ca..96f48c224 100644 --- a/pkg/common/utils.go +++ b/pkg/common/utils.go @@ -2,6 +2,7 @@ package common import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -163,6 +164,32 @@ func GetHCPNamespace(name, namespace string) string { return fmt.Sprintf("%s-%s", namespace, name) } +// GetHostedCluster finds the HostedCluster that owns the HCP by deriving +// its namespace and name from the HCP namespace convention: {hc-namespace}-{hc-name}. +func GetHostedCluster(ctx context.Context, c crclient.Client, includedNamespaces []string, hcpNamespace string) (*hyperv1.HostedCluster, error) { + var errs []error + for _, ns := range includedNamespaces { + if ns == hcpNamespace { + continue + } + hcList := &hyperv1.HostedClusterList{} + if err := c.List(ctx, hcList, crclient.InNamespace(ns)); err != nil { + errs = append(errs, fmt.Errorf("list HostedClusters in namespace %s: %w", ns, err)) + continue + } + for i := range hcList.Items { + hc := &hcList.Items[i] + if GetHCPNamespace(hc.Name, hc.Namespace) == hcpNamespace { + return hc, nil + } + } + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return nil, nil +} + // ShouldEndPluginExecution checks if the plugin should end execution by verifying if the required // Hypershift resources (HostedControlPlane and HostedCluster) exist in the cluster. // Returns true if the plugin should end execution (i.e., if this is not a Hypershift cluster). diff --git a/pkg/common/utils_test.go b/pkg/common/utils_test.go index 43c95300f..eacfb8871 100644 --- a/pkg/common/utils_test.go +++ b/pkg/common/utils_test.go @@ -573,6 +573,34 @@ func TestShouldEndPluginExecution(t *testing.T) { } } +func TestGetHostedCluster(t *testing.T) { + scheme := runtime.NewScheme() + _ = hyperv1.AddToScheme(scheme) + + t.Run("When GetHostedCluster runs with a HostedCluster matching HCP namespace, It Should return that cluster", func(t *testing.T) { + g := NewWithT(t) + hc := &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "my-cluster", Namespace: "clusters"}, + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(hc).Build() + + result, err := GetHostedCluster(context.TODO(), c, []string{"clusters", "clusters-my-cluster"}, "clusters-my-cluster") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result).NotTo(BeNil()) + g.Expect(result.Name).To(Equal("my-cluster")) + g.Expect(result.Namespace).To(Equal("clusters")) + }) + + t.Run("When GetHostedCluster runs with no HostedClusters in client, It Should return nil", func(t *testing.T) { + g := NewWithT(t) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + + result, err := GetHostedCluster(context.TODO(), c, []string{"clusters", "clusters-my-cluster"}, "clusters-my-cluster") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result).To(BeNil()) + }) +} + func TestCRDExists(t *testing.T) { scheme := runtime.NewScheme() _ = hyperv1.AddToScheme(scheme) diff --git a/pkg/core/backup.go b/pkg/core/backup.go index 98f3c4ec6..d4fc55f15 100644 --- a/pkg/core/backup.go +++ b/pkg/core/backup.go @@ -8,6 +8,7 @@ import ( common "github.com/openshift/hypershift-oadp-plugin/pkg/common" plugtypes "github.com/openshift/hypershift-oadp-plugin/pkg/core/types" validation "github.com/openshift/hypershift-oadp-plugin/pkg/core/validation" + "github.com/openshift/hypershift-oadp-plugin/pkg/etcdbackup" "github.com/openshift/hypershift-oadp-plugin/pkg/platform/agent" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/sirupsen/logrus" @@ -31,6 +32,12 @@ type BackupPlugin struct { validator validation.BackupValidator hcp *hyperv1.HostedControlPlane *plugtypes.BackupOptions + + // Etcd backup orchestration + etcdOrchestrator *etcdbackup.Orchestrator + hoNamespace string + etcdBackupMethod string + etcdSnapshotURL string // populated after HCPEtcdBackup completes } // NewBackupPlugin instantiates BackupPlugin. @@ -71,12 +78,27 @@ func NewBackupPlugin(logger logrus.FieldLogger) (*BackupPlugin, error) { Client: client, } + hoNamespace := common.DefaultHONamespace + if v, ok := pluginConfig.Data[common.ConfigKeyHONamespace]; ok && v != "" { + hoNamespace = v + } + + etcdBackupMethod := common.EtcdBackupMethodVolume + if v, ok := pluginConfig.Data[common.ConfigKeyEtcdBackupMethod]; ok && v != "" { + etcdBackupMethod = v + } + if etcdBackupMethod != common.EtcdBackupMethodVolume && etcdBackupMethod != common.EtcdBackupMethodEtcdSnapshot { + return nil, fmt.Errorf("invalid etcdBackupMethod %q: must be %q or %q", etcdBackupMethod, common.EtcdBackupMethodVolume, common.EtcdBackupMethodEtcdSnapshot) + } + bp := &BackupPlugin{ - log: logger, - client: client, - config: pluginConfig.Data, - ctx: ctx, - validator: validator, + log: logger, + client: client, + config: pluginConfig.Data, + ctx: ctx, + validator: validator, + hoNamespace: hoNamespace, + etcdBackupMethod: etcdBackupMethod, } if bp.BackupOptions, err = bp.validator.ValidatePluginConfig(bp.config); err != nil { @@ -119,7 +141,6 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu } return nil, nil, fmt.Errorf("error getting HCP namespace: %v", err) } - } kind := item.GetObjectKind().GroupVersionKind().Kind @@ -134,6 +155,16 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu return nil, nil, fmt.Errorf("error checking platform configuration: %v", err) } + // Etcd backup: create after validation, wait for completion + if p.etcdBackupMethod == common.EtcdBackupMethodEtcdSnapshot { + if err := p.createEtcdBackup(ctx, backup); err != nil { + return nil, nil, fmt.Errorf("error creating HCPEtcdBackup: %v", err) + } + } + if err := p.waitForEtcdBackupCompletion(ctx); err != nil { + return nil, nil, err + } + case kind == common.HostedClusterKind: metadata, err := meta.Accessor(item) if err != nil { @@ -142,16 +173,53 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu common.AddAnnotation(metadata, common.HostedClusterRestoredFromBackupAnnotation, "") p.log.Infof("Added restore annotation to HostedCluster %s", metadata.GetName()) - case kind == "Pod": - // In case of FSBackup, we need to add the label to the pod - if backup.Spec.DefaultVolumesToFsBackup != nil && !*backup.Spec.DefaultVolumesToFsBackup { - metadata, err := meta.Accessor(item) - if err != nil { - return nil, nil, fmt.Errorf("error getting metadata accessor: %v", err) + // Etcd backup: create if not yet created (HC may arrive before HCP), + // wait for completion, and inject snapshotURL into the HC item. + // Velero captures the item as-is from the API server before the HCPEtcdBackup + // controller updates the HC status with lastSuccessfulEtcdBackupURL. + // We must inject it here so the backed-up HC contains the URL for restore. + if p.etcdBackupMethod == common.EtcdBackupMethodEtcdSnapshot { + if err := p.createEtcdBackup(ctx, backup); err != nil { + return nil, nil, fmt.Errorf("error creating HCPEtcdBackup: %v", err) } + } + if err := p.waitForEtcdBackupCompletion(ctx); err != nil { + return nil, nil, err + } + if p.etcdSnapshotURL != "" { + // Persist as annotation so the restore plugin can read it + // (Velero strips status from items during restore) + common.AddAnnotation(metadata, common.EtcdSnapshotURLAnnotation, p.etcdSnapshotURL) + p.log.Infof("Added etcd snapshot URL annotation to HostedCluster %s: %s", metadata.GetName(), p.etcdSnapshotURL) - if strings.Contains(metadata.GetName(), "etcd-") { - common.AddLabel(metadata, common.FSBackupLabelName, "true") + unstructuredContent := item.UnstructuredContent() + status, ok := unstructuredContent["status"].(map[string]interface{}) + if !ok { + status = map[string]interface{}{} + unstructuredContent["status"] = status + } + status["lastSuccessfulEtcdBackupURL"] = p.etcdSnapshotURL + item.SetUnstructuredContent(unstructuredContent) + p.log.Infof("Injected lastSuccessfulEtcdBackupURL into HostedCluster %s: %s", metadata.GetName(), p.etcdSnapshotURL) + } + + case kind == "Pod": + metadata, err := meta.Accessor(item) + if err != nil { + return nil, nil, fmt.Errorf("error getting metadata accessor: %v", err) + } + + if strings.Contains(metadata.GetName(), "etcd-") { + switch p.etcdBackupMethod { + case common.EtcdBackupMethodEtcdSnapshot: + // Skip etcd pods entirely, snapshot is handled by HCPEtcdBackup. + // This prevents both FSBackup and CSI VolumeSnapshots of etcd volumes. + p.log.Infof("Skipping etcd pod %s from backup (using etcdSnapshot method)", metadata.GetName()) + return nil, nil, nil + case common.EtcdBackupMethodVolume: + if backup.Spec.DefaultVolumesToFsBackup != nil && !*backup.Spec.DefaultVolumesToFsBackup { + common.AddLabel(metadata, common.FSBackupLabelName, "true") + } } } @@ -172,7 +240,91 @@ func (p *BackupPlugin) Execute(item runtime.Unstructured, backup *velerov1.Backu if _, exists := labels[common.KubevirtRHCOSLabel]; exists { return nil, nil, nil } + + // Exclude etcd data PVCs when using etcdSnapshot method. + // PVC names follow the StatefulSet pattern: data-etcd-{index} + if kind == common.PersistentVolumeClaimKind && + strings.HasPrefix(metadata.GetName(), common.EtcdPVCPrefix) && + p.etcdBackupMethod == common.EtcdBackupMethodEtcdSnapshot { + p.log.Infof("Excluding etcd PVC %s from backup (using etcdSnapshot method)", metadata.GetName()) + return nil, nil, nil + } } return item, nil, nil } + +// createEtcdBackup creates an HCPEtcdBackup CR in the HCP namespace. +// It is idempotent: if the orchestrator already created a backup, it returns immediately. +// Requires the HCPEtcdBackup CRD to exist in the cluster (safenet check). +func (p *BackupPlugin) createEtcdBackup(ctx context.Context, backup *velerov1.Backup) error { + // Already created by a previous Execute() call + if p.etcdOrchestrator != nil && p.etcdOrchestrator.IsCreated() { + return nil + } + + crdExists, err := common.CRDExists(ctx, "hcpetcdbackups.hypershift.openshift.io", p.client) + if err != nil { + return fmt.Errorf("failed to check for HCPEtcdBackup CRD: %w", err) + } + if !crdExists { + return fmt.Errorf("etcdBackupMethod is %q but HCPEtcdBackup CRD not found in the cluster", common.EtcdBackupMethodEtcdSnapshot) + } + + oadpNS, err := common.GetCurrentNamespace() + if err != nil { + return fmt.Errorf("failed to get OADP namespace: %w", err) + } + + p.etcdOrchestrator = etcdbackup.NewOrchestrator(p.log, p.client, p.hoNamespace, oadpNS) + + // Fetch the HostedCluster for encryption config + hc, err := common.GetHostedCluster(ctx, p.client, backup.Spec.IncludedNamespaces, p.hcp.Namespace) + if err != nil { + p.log.Warnf("Could not find HostedCluster for encryption config: %v", err) + } + + if err := p.etcdOrchestrator.CreateEtcdBackup(ctx, backup, p.hcp.Namespace, hc); err != nil { + if cleanupErr := p.etcdOrchestrator.CleanupCredentialSecret(ctx); cleanupErr != nil { + p.log.Warnf("Failed to cleanup credential Secret after create error: %v", cleanupErr) + } + return err + } + + if err := p.etcdOrchestrator.VerifyInProgress(ctx); err != nil { + if cleanupErr := p.etcdOrchestrator.CleanupCredentialSecret(ctx); cleanupErr != nil { + p.log.Warnf("Failed to cleanup credential Secret after verify error: %v", cleanupErr) + } + return err + } + + return nil +} + +// waitForEtcdBackupCompletion waits for the HCPEtcdBackup to finish and cleans up +// the copied credential Secret. Caches the snapshotURL on the plugin struct so it +// is available regardless of item processing order (HC before HCP or vice versa). +// It is a no-op if no etcd backup was created. +func (p *BackupPlugin) waitForEtcdBackupCompletion(ctx context.Context) error { + if p.etcdOrchestrator == nil || !p.etcdOrchestrator.IsCreated() { + return nil + } + + // Already completed in a previous Execute() call + if p.etcdSnapshotURL != "" { + return nil + } + + snapshotURL, err := p.etcdOrchestrator.WaitForCompletion(ctx) + if err != nil { + return fmt.Errorf("HCPEtcdBackup failed: %v", err) + } + p.etcdSnapshotURL = snapshotURL + p.log.Infof("HCPEtcdBackup completed, snapshotURL: %s", snapshotURL) + + if cleanupErr := p.etcdOrchestrator.CleanupCredentialSecret(ctx); cleanupErr != nil { + p.log.Warnf("Failed to cleanup etcd backup credential Secret: %v", cleanupErr) + } + + return nil +} diff --git a/pkg/core/types/types.go b/pkg/core/types/types.go index 36bd7faa1..5cd0d1944 100644 --- a/pkg/core/types/types.go +++ b/pkg/core/types/types.go @@ -3,6 +3,7 @@ package types var ( BackupCommonResources = []string{ "hostedclusters", "hostedcluster", "hostedcontrolplanes", "hostedcontrolplane", "nodepools", "nodepool", + "hcpetcdbackups", "hcpetcdbackup", "secrets", "secret", "configmaps", "configmap", "persistentvolumes", "persistentvolume", "persistentvolumeclaims", "persistentvolumeclaim", "pods", "pod", "statefulsets", "statefulset", "deployments", "deployment", "clusters", "cluster", "machines", "machine", "machinedeployments", "machinedeployment", "machinesets", "machineset", "serviceaccounts", "serviceaccount", "roles", "role", "rolebindings", "rolebinding", @@ -20,17 +21,9 @@ var ( type BackupOptions struct { // Migration is a flag to indicate if the backup is for migration purposes. Migration bool - // Readopt Nodes is a flag to indicate if the nodes should be reprovisioned or not during restore. - ReadoptNodes bool - // ManagedServices is a flag to indicate if the backup is done for ManagedServices like ROSA, ARO, etc. - ManagedServices bool } type RestoreOptions struct { // Migration is a flag to indicate if the backup is for migration purposes. Migration bool - // Readopt Nodes is a flag to indicate if the nodes should be reprovisioned or not during restore. - ReadoptNodes bool - // ManagedServices is a flag to indicate if the backup is done for ManagedServices like ROSA, ARO, etc. - ManagedServices bool } diff --git a/pkg/core/validation/backup.go b/pkg/core/validation/backup.go index d0b3f341f..1d5f2d70d 100644 --- a/pkg/core/validation/backup.go +++ b/pkg/core/validation/backup.go @@ -35,12 +35,8 @@ func (p *BackupPluginValidator) ValidatePluginConfig(config map[string]string) ( case "migration": p.Log.Debugf("reading/parsing migration %s", value) bo.Migration = value == "true" - case "readoptNodes": - p.Log.Debugf("reading/parsing readoptNodes %s", value) - bo.ReadoptNodes = value == "true" - case "managedServices": - p.Log.Debugf("reading/parsing managedServices %s", value) - bo.ManagedServices = value == "true" + case "etcdBackupMethod", "hoNamespace": + p.Log.Debugf("configuration key %s=%s handled by plugin init", key, value) default: p.Log.Warnf("unknown configuration key: %s with value %s", key, value) } diff --git a/pkg/core/validation/backup_test.go b/pkg/core/validation/backup_test.go index 8310b6f80..b4982aecf 100644 --- a/pkg/core/validation/backup_test.go +++ b/pkg/core/validation/backup_test.go @@ -21,11 +21,23 @@ func TestValidatePluginConfig(t *testing.T) { expectError: false, }, { - name: "valid config with all options", + name: "valid config with migration", config: map[string]string{ - "migration": "true", - "readoptNodes": "true", - "managedServices": "true", + "migration": "true", + }, + expectError: false, + }, + { + name: "When config contains etcdBackupMethod, It Should accept it without error", + config: map[string]string{ + "etcdBackupMethod": "etcdSnapshot", + }, + expectError: false, + }, + { + name: "When config contains hoNamespace, It Should accept it without error", + config: map[string]string{ + "hoNamespace": "my-hypershift", }, expectError: false, }, diff --git a/pkg/core/validation/restore.go b/pkg/core/validation/restore.go index d4e1dd291..f406b3966 100644 --- a/pkg/core/validation/restore.go +++ b/pkg/core/validation/restore.go @@ -4,7 +4,6 @@ import ( "fmt" plugtypes "github.com/openshift/hypershift-oadp-plugin/pkg/core/types" - aws "github.com/openshift/hypershift-oadp-plugin/pkg/platform/aws" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/sirupsen/logrus" crclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,12 +35,8 @@ func (p *RestorePluginValidator) ValidatePluginConfig(config map[string]string) case "migration": p.Log.Debugf("reading/parsing migration %s", value) bo.Migration = value == "true" - case "readoptNodes": - p.Log.Debugf("reading/parsing readoptNodes %s", value) - bo.ReadoptNodes = value == "true" - case "managedServices": - p.Log.Debugf("reading/parsing managedServices %s", value) - bo.ManagedServices = value == "true" + case "etcdBackupMethod", "hoNamespace": + p.Log.Debugf("configuration key %s=%s handled by plugin init", key, value) default: p.Log.Warnf("unknown configuration key: %s with value %s", key, value) } @@ -77,13 +72,6 @@ func (p *RestorePluginValidator) validateAWSPlatform(hcp *hyperv1.HostedControlP // Validate if the AWS platform is configured properly // Validate ROSA p.Log.Infof("%s AWS platform configuration is valid for HCP: %s", p.LogHeader, hcp.Name) - - if config["managedServices"] == "true" || config["awsRegenPrivateLink"] == "true" { - p.Log.Infof("%s AWS platform restore tasks for HCP: %s", p.LogHeader, hcp.Name) - if err := aws.RestoreTasks(hcp, p.Client); err != nil { - return fmt.Errorf("error executing ROSA platform restore tasks: %s", err.Error()) - } - } return nil } diff --git a/pkg/etcdbackup/orchestrator.go b/pkg/etcdbackup/orchestrator.go new file mode 100644 index 000000000..b2f9d1419 --- /dev/null +++ b/pkg/etcdbackup/orchestrator.go @@ -0,0 +1,365 @@ +package etcdbackup + +import ( + "context" + "fmt" + "time" + + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/sirupsen/logrus" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/wait" + crclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + verifyTimeout = 30 * time.Second + completionTimeout = 10 * time.Minute + pollInterval = 5 * time.Second +) + +// Orchestrator manages the lifecycle of HCPEtcdBackup CRs during OADP backup. +type Orchestrator struct { + log logrus.FieldLogger + client crclient.Client + + // State tracked across Execute() calls + BackupName string + BackupNamespace string + HONamespace string + OADPNamespace string + CredSecretName string +} + +// NewOrchestrator creates a new Orchestrator. +func NewOrchestrator(log logrus.FieldLogger, client crclient.Client, hoNamespace, oadpNamespace string) *Orchestrator { + return &Orchestrator{ + log: log.WithField("component", "etcdbackup-orchestrator"), + client: client, + HONamespace: hoNamespace, + OADPNamespace: oadpNamespace, + } +} + +// IsCreated returns true if an HCPEtcdBackup CR was created. +func (o *Orchestrator) IsCreated() bool { + return o.BackupName != "" +} + +// CreateEtcdBackup creates an HCPEtcdBackup CR in the HCP namespace. +// It fetches the BSL, copies credentials, and maps storage config. +func (o *Orchestrator) CreateEtcdBackup(ctx context.Context, backup *velerov1.Backup, hcpNamespace string, hc *hyperv1.HostedCluster) error { + bsl, err := o.fetchBSL(ctx, backup.Spec.StorageLocation, o.OADPNamespace) + if err != nil { + return fmt.Errorf("failed to fetch BackupStorageLocation %q: %w", backup.Spec.StorageLocation, err) + } + + storage, err := o.mapBSLToStorage(bsl, backup.Name) + if err != nil { + return fmt.Errorf("failed to map BSL to HCPEtcdBackup storage: %w", err) + } + + credSecretName, err := o.copyCredentialSecret(ctx, bsl, o.OADPNamespace, o.HONamespace, backup.Name) + if err != nil { + return fmt.Errorf("failed to copy credential Secret: %w", err) + } + o.CredSecretName = credSecretName + + // Set credential reference on the storage config + setCredentialRef(storage, credSecretName) + + // Set encryption fields from HostedCluster if available + if hc != nil { + setEncryptionFields(storage, hc) + } + + crName := fmt.Sprintf("oadp-%s-%s", backup.Name, utilrand.String(4)) + etcdBackup := &hyperv1.HCPEtcdBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + Namespace: hcpNamespace, + }, + Spec: hyperv1.HCPEtcdBackupSpec{ + Storage: *storage, + }, + } + + if err := o.client.Create(ctx, etcdBackup); err != nil { + if apierrors.IsAlreadyExists(err) { + o.log.Infof("HCPEtcdBackup %s/%s already exists, reusing", hcpNamespace, crName) + } else { + return fmt.Errorf("failed to create HCPEtcdBackup: %w", err) + } + } else { + o.log.Infof("Created HCPEtcdBackup %s/%s", hcpNamespace, crName) + } + + o.BackupName = crName + o.BackupNamespace = hcpNamespace + return nil +} + +// VerifyInProgress polls the HCPEtcdBackup until the controller acknowledges it. +func (o *Orchestrator) VerifyInProgress(ctx context.Context) error { + return o.pollCondition(ctx, verifyTimeout, func(cond *metav1.Condition) (bool, error) { + if cond == nil { + return false, nil // no condition yet, keep polling + } + switch cond.Reason { + case hyperv1.BackupInProgressReason: + o.log.Info("HCPEtcdBackup is in progress") + return true, nil + case hyperv1.BackupSucceededReason: + o.log.Info("HCPEtcdBackup already succeeded") + return true, nil + case hyperv1.BackupFailedReason: + return false, fmt.Errorf("HCPEtcdBackup failed: %s", cond.Message) + case hyperv1.BackupRejectedReason: + return false, fmt.Errorf("HCPEtcdBackup rejected: %s", cond.Message) + case hyperv1.EtcdUnhealthyReason: + return false, fmt.Errorf("etcd unhealthy: %s", cond.Message) + } + return false, nil + }) +} + +// WaitForCompletion polls the HCPEtcdBackup until it reaches a terminal state. +// Returns the snapshotURL on success. +func (o *Orchestrator) WaitForCompletion(ctx context.Context) (string, error) { + var snapshotURL string + + err := o.pollCondition(ctx, completionTimeout, func(cond *metav1.Condition) (bool, error) { + if cond == nil { + return false, nil + } + + // Success: status=True + if cond.Status == metav1.ConditionTrue && cond.Reason == hyperv1.BackupSucceededReason { + // Re-fetch to get snapshotURL + eb := &hyperv1.HCPEtcdBackup{} + if err := o.client.Get(ctx, types.NamespacedName{Name: o.BackupName, Namespace: o.BackupNamespace}, eb); err != nil { + return false, fmt.Errorf("failed to get HCPEtcdBackup for snapshotURL: %w", err) + } + snapshotURL = eb.Status.SnapshotURL + o.log.Infof("HCPEtcdBackup completed successfully, snapshotURL: %s", snapshotURL) + return true, nil + } + + // Terminal failures + switch cond.Reason { + case hyperv1.BackupFailedReason: + return false, fmt.Errorf("HCPEtcdBackup failed: %s", cond.Message) + case hyperv1.BackupRejectedReason: + return false, fmt.Errorf("HCPEtcdBackup rejected: %s", cond.Message) + } + + // Still in progress + return false, nil + }) + + if err != nil { + return "", err + } + return snapshotURL, nil +} + +// CleanupCredentialSecret removes the copied credential Secret from the HO namespace. +func (o *Orchestrator) CleanupCredentialSecret(ctx context.Context) error { + if o.CredSecretName == "" { + return nil + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: o.CredSecretName, + Namespace: o.HONamespace, + }, + } + if err := o.client.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete credential Secret %s/%s: %w", o.HONamespace, o.CredSecretName, err) + } + o.log.Infof("Cleaned up credential Secret %s/%s", o.HONamespace, o.CredSecretName) + return nil +} + +// fetchBSL retrieves the BackupStorageLocation from the OADP namespace. +func (o *Orchestrator) fetchBSL(ctx context.Context, bslName, namespace string) (*velerov1.BackupStorageLocation, error) { + bsl := &velerov1.BackupStorageLocation{} + if err := o.client.Get(ctx, types.NamespacedName{Name: bslName, Namespace: namespace}, bsl); err != nil { + return nil, err + } + return bsl, nil +} + +// mapBSLToStorage translates a Velero BackupStorageLocation into hyperv1.HCPEtcdBackupStorage +// so the etcd backup controller can use the same object store as OADP/Velero. +// +// The KeyPrefix follows Velero's backup directory layout: +// {bsl-prefix}/backups/{backup-name}/etcd-backup +// so the etcd snapshot is stored alongside the rest of the backup data. +func (o *Orchestrator) mapBSLToStorage(bsl *velerov1.BackupStorageLocation, backupName string) (*hyperv1.HCPEtcdBackupStorage, error) { + keyPrefix := fmt.Sprintf("backups/%s/etcd-backup", backupName) + if bsl.Spec.ObjectStorage != nil && bsl.Spec.ObjectStorage.Prefix != "" { + keyPrefix = fmt.Sprintf("%s/backups/%s/etcd-backup", bsl.Spec.ObjectStorage.Prefix, backupName) + } + + switch bsl.Spec.Provider { + case "aws", "velero.io/aws": + return mapAWSBSLToStorage(bsl, keyPrefix), nil + case "azure", "velero.io/azure": + return mapAzureBSLToStorage(bsl, keyPrefix), nil + } + + return nil, fmt.Errorf("unsupported BSL provider %q: only aws and azure are supported", bsl.Spec.Provider) +} + +// mapAWSBSLToStorage maps a Velero AWS BackupStorageLocation to HCPEtcdBackup S3 storage. +func mapAWSBSLToStorage(bsl *velerov1.BackupStorageLocation, keyPrefix string) *hyperv1.HCPEtcdBackupStorage { + bucket := "" + if bsl.Spec.ObjectStorage != nil { + bucket = bsl.Spec.ObjectStorage.Bucket + } + region := "" + if bsl.Spec.Config != nil { + region = bsl.Spec.Config["region"] + } + return &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.S3BackupStorage, + S3: hyperv1.HCPEtcdBackupS3{ + Bucket: bucket, + Region: region, + KeyPrefix: keyPrefix, + }, + } +} + +// mapAzureBSLToStorage maps a Velero Azure BackupStorageLocation to HCPEtcdBackup Azure Blob storage. +func mapAzureBSLToStorage(bsl *velerov1.BackupStorageLocation, keyPrefix string) *hyperv1.HCPEtcdBackupStorage { + container := "" + if bsl.Spec.ObjectStorage != nil { + container = bsl.Spec.ObjectStorage.Bucket + } + storageAccount := "" + if bsl.Spec.Config != nil { + storageAccount = bsl.Spec.Config["storageAccount"] + } + return &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.AzureBlobBackupStorage, + AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{ + Container: container, + StorageAccount: storageAccount, + KeyPrefix: keyPrefix, + }, + } +} + +// copyCredentialSecret copies the BSL credential Secret to the HO namespace, +// remapping the data key from the BSL's key (typically "cloud") to "credentials" +// as expected by the HCPEtcdBackup controller. +// If the destination Secret already exists, it is reused. The credential data +// contains an STS IAM Role ARN (not rotatable keys), so it is safe to reuse. +func (o *Orchestrator) copyCredentialSecret(ctx context.Context, bsl *velerov1.BackupStorageLocation, fromNS, toNS, backupName string) (string, error) { + if bsl.Spec.Credential == nil { + return "", fmt.Errorf("BSL %q has no credential reference", bsl.Name) + } + + dstName := fmt.Sprintf("etcd-backup-creds-%s", backupName) + + // Check if the destination Secret already exists + if err := o.client.Get(ctx, types.NamespacedName{Name: dstName, Namespace: toNS}, &corev1.Secret{}); err == nil { + o.log.Infof("Credential Secret %s/%s already exists, reusing", toNS, dstName) + return dstName, nil + } + + srcSecret := &corev1.Secret{} + if err := o.client.Get(ctx, types.NamespacedName{ + Name: bsl.Spec.Credential.Name, + Namespace: fromNS, + }, srcSecret); err != nil { + return "", fmt.Errorf("failed to get BSL credential Secret %s/%s: %w", fromNS, bsl.Spec.Credential.Name, err) + } + + // The BSL references a specific key in the Secret (e.g. "cloud"). + // The HCPEtcdBackup controller mounts the Secret as a volume and reads + // the file at /etc/etcd-backup-creds/credentials, so we remap the key. + srcKey := bsl.Spec.Credential.Key + credData, ok := srcSecret.Data[srcKey] + if !ok { + return "", fmt.Errorf("BSL credential Secret %s/%s does not contain key %q", fromNS, bsl.Spec.Credential.Name, srcKey) + } + + dstSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: dstName, + Namespace: toNS, + Labels: map[string]string{ + "hypershift.openshift.io/etcd-backup": "true", + }, + }, + Data: map[string][]byte{ + "credentials": credData, + }, + } + + if err := o.client.Create(ctx, dstSecret); err != nil { + return "", fmt.Errorf("failed to create credential Secret %s/%s: %w", toNS, dstName, err) + } + + o.log.Infof("Copied credential Secret to %s/%s (remapped key %q -> credentials)", toNS, dstName, srcKey) + return dstName, nil +} + +// setCredentialRef sets the credential reference on the storage config. +func setCredentialRef(storage *hyperv1.HCPEtcdBackupStorage, secretName string) { + ref := hyperv1.SecretReference{Name: secretName} + switch storage.StorageType { + case hyperv1.S3BackupStorage: + storage.S3.Credentials = ref + case hyperv1.AzureBlobBackupStorage: + storage.AzureBlob.Credentials = ref + } +} + +// setEncryptionFields sets encryption config from HostedCluster's ManagedEtcdSpec.Backup. +func setEncryptionFields(storage *hyperv1.HCPEtcdBackupStorage, hc *hyperv1.HostedCluster) { + if hc.Spec.Etcd.Managed == nil { + return + } + backupConfig := hc.Spec.Etcd.Managed.Backup + + switch storage.StorageType { + case hyperv1.S3BackupStorage: + if backupConfig.Platform == hyperv1.AWSBackupConfigPlatform && backupConfig.AWS.KMSKeyARN != "" { + storage.S3.KMSKeyARN = backupConfig.AWS.KMSKeyARN + } + case hyperv1.AzureBlobBackupStorage: + if backupConfig.Platform == hyperv1.AzureBackupConfigPlatform && backupConfig.Azure.EncryptionKeyURL != "" { + storage.AzureBlob.EncryptionKeyURL = backupConfig.Azure.EncryptionKeyURL + } + } +} + +// pollCondition polls the HCPEtcdBackup's BackupCompleted condition until the check function +// returns true (done) or an error (terminal failure), or until timeout. +// The first check runs immediately (before the first interval wait). +func (o *Orchestrator) pollCondition(ctx context.Context, timeout time.Duration, check func(*metav1.Condition) (bool, error)) error { + return wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, func(ctx context.Context) (bool, error) { + eb := &hyperv1.HCPEtcdBackup{} + if err := o.client.Get(ctx, types.NamespacedName{Name: o.BackupName, Namespace: o.BackupNamespace}, eb); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("failed to get HCPEtcdBackup: %w", err) + } + + cond := meta.FindStatusCondition(eb.Status.Conditions, string(hyperv1.BackupCompleted)) + return check(cond) + }) +} + diff --git a/pkg/etcdbackup/orchestrator_test.go b/pkg/etcdbackup/orchestrator_test.go new file mode 100644 index 000000000..39f4818e4 --- /dev/null +++ b/pkg/etcdbackup/orchestrator_test.go @@ -0,0 +1,544 @@ +package etcdbackup + +// Test scenario names follow: "When , It Should ". + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/sirupsen/logrus" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func testScheme() *runtime.Scheme { + s := runtime.NewScheme() + _ = hyperv1.AddToScheme(s) + _ = velerov1.AddToScheme(s) + _ = corev1.AddToScheme(s) + return s +} + +// mapBSLCase describes one row in TestMapBSLToStorage: either mapBSLToStorage (full path +// including key-prefix resolution) or a direct call to mapAWSBSLToStorage / mapAzureBSLToStorage. +type mapBSLCase struct { + name string + how mapBSLCaseHow // mapBSL | mapAWS | mapAzure + bsl *velerov1.BackupStorageLocation + mapperPrefix string // second arg to mapAWS/mapAzure; ignored for mapBSL + wantErr bool + errSubstr string + assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) +} + +type mapBSLCaseHow int + +const ( + mapViaBSL mapBSLCaseHow = iota + mapViaAWS + mapViaAzure +) + +// TestMapBSLToStorage covers mapBSLToStorage (AWS, Azure, unsupported, key prefixes) and +// mapAWSBSLToStorage / mapAzureBSLToStorage in one table. +func TestMapBSLToStorage(t *testing.T) { + o := &Orchestrator{log: logrus.New()} + tests := []mapBSLCase{ + { + name: "When mapBSLToStorage runs for AWS BSL with bucket and object storage prefix, It Should set S3 bucket region and key prefix", + how: mapViaBSL, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "my-bucket", + Prefix: "velero-backups", + }, + }, + Config: map[string]string{"region": "us-east-1"}, + }, + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.StorageType).To(Equal(hyperv1.S3BackupStorage)) + g.Expect(s.S3.Bucket).To(Equal("my-bucket")) + g.Expect(s.S3.Region).To(Equal("us-east-1")) + g.Expect(s.S3.KeyPrefix).To(Equal("velero-backups/backups/test-backup/etcd-backup")) + }, + }, + { + name: "When mapBSLToStorage runs for Azure BSL with container and prefix, It Should set Azure blob container account and key prefix", + how: mapViaBSL, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "azure", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "my-container", + Prefix: "velero", + }, + }, + Config: map[string]string{"storageAccount": "mystorageaccount"}, + }, + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.StorageType).To(Equal(hyperv1.AzureBlobBackupStorage)) + g.Expect(s.AzureBlob.Container).To(Equal("my-container")) + g.Expect(s.AzureBlob.StorageAccount).To(Equal("mystorageaccount")) + g.Expect(s.AzureBlob.KeyPrefix).To(Equal("velero/backups/test-backup/etcd-backup")) + }, + }, + { + name: "When mapBSLToStorage runs for GCP BSL, It Should return unsupported provider error", + how: mapViaBSL, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{Provider: "gcp"}, + }, + wantErr: true, + errSubstr: "unsupported BSL provider", + }, + { + name: "When mapBSLToStorage runs for AWS BSL without object storage prefix, It Should default key prefix to etcd-backup", + how: mapViaBSL, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-bucket"}, + }, + Config: map[string]string{"region": "eu-west-1"}, + }, + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.KeyPrefix).To(Equal("backups/test-backup/etcd-backup")) + }, + }, + { + name: "When mapBSLToStorage runs for velero.io/aws BSL with prefix, It Should include prefix and backup name in key prefix", + how: mapViaBSL, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "velero.io/aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "my-bucket", + Prefix: "my-prefix", + }, + }, + Config: map[string]string{"region": "us-west-2"}, + }, + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.StorageType).To(Equal(hyperv1.S3BackupStorage)) + g.Expect(s.S3.KeyPrefix).To(Equal("my-prefix/backups/test-backup/etcd-backup")) + }, + }, + { + name: "When mapAWSBSLToStorage runs with bucket region and mapper prefix, It Should set S3 bucket region and key prefix", + how: mapViaAWS, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-bucket"}, + }, + Config: map[string]string{"region": "eu-central-1"}, + }, + }, + mapperPrefix: "backups/etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.StorageType).To(Equal(hyperv1.S3BackupStorage)) + g.Expect(s.S3.Bucket).To(Equal("my-bucket")) + g.Expect(s.S3.Region).To(Equal("eu-central-1")) + g.Expect(s.S3.KeyPrefix).To(Equal("backups/etcd-backup")) + }, + }, + { + name: "When mapAWSBSLToStorage runs without object storage, It Should leave bucket empty and preserve region", + how: mapViaAWS, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Config: map[string]string{"region": "us-east-1"}, + }, + }, + mapperPrefix: "etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.Bucket).To(BeEmpty()) + g.Expect(s.S3.Region).To(Equal("us-east-1")) + g.Expect(s.S3.KeyPrefix).To(Equal("etcd-backup")) + }, + }, + { + name: "When mapAWSBSLToStorage runs without config, It Should leave region empty", + how: mapViaAWS, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "b"}, + }, + }, + }, + mapperPrefix: "etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.Bucket).To(Equal("b")) + g.Expect(s.S3.Region).To(BeEmpty()) + }, + }, + { + name: "When mapAzureBSLToStorage runs with container and storage account, It Should set Azure blob fields and key prefix", + how: mapViaAzure, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-container"}, + }, + Config: map[string]string{"storageAccount": "acct"}, + }, + }, + mapperPrefix: "velero/etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.StorageType).To(Equal(hyperv1.AzureBlobBackupStorage)) + g.Expect(s.AzureBlob.Container).To(Equal("my-container")) + g.Expect(s.AzureBlob.StorageAccount).To(Equal("acct")) + g.Expect(s.AzureBlob.KeyPrefix).To(Equal("velero/etcd-backup")) + }, + }, + { + name: "When mapAzureBSLToStorage runs without object storage, It Should leave container empty", + how: mapViaAzure, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + Config: map[string]string{"storageAccount": "sa"}, + }, + }, + mapperPrefix: "etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.AzureBlob.Container).To(BeEmpty()) + g.Expect(s.AzureBlob.StorageAccount).To(Equal("sa")) + }, + }, + { + name: "When mapAzureBSLToStorage runs without config, It Should leave storage account empty", + how: mapViaAzure, + bsl: &velerov1.BackupStorageLocation{ + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "c"}, + }, + }, + }, + mapperPrefix: "etcd-backup", + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.AzureBlob.Container).To(Equal("c")) + g.Expect(s.AzureBlob.StorageAccount).To(BeEmpty()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + var storage *hyperv1.HCPEtcdBackupStorage + var err error + switch tt.how { + case mapViaBSL: + storage, err = o.mapBSLToStorage(tt.bsl, "test-backup") + case mapViaAWS: + storage = mapAWSBSLToStorage(tt.bsl, tt.mapperPrefix) + case mapViaAzure: + storage = mapAzureBSLToStorage(tt.bsl, tt.mapperPrefix) + } + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + if tt.errSubstr != "" { + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + } + return + } + g.Expect(err).NotTo(HaveOccurred()) + tt.assert(g, storage) + }) + } +} + +func TestCopyCredentialSecret(t *testing.T) { + scheme := testScheme() + + t.Run("When copyCredentialSecret runs with BSL credential key cloud, It Should remap to credentials key in destination", func(t *testing.T) { + g := NewWithT(t) + + srcSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloud-credentials", + Namespace: "openshift-adp", + }, + Data: map[string][]byte{"cloud": []byte("aws-creds-data")}, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(srcSecret).Build() + o := &Orchestrator{log: logrus.New(), client: client} + + bsl := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + } + + dstName, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) + + copied := &corev1.Secret{} + err = client.Get(context.TODO(), types.NamespacedName{Name: dstName, Namespace: "hypershift"}, copied) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(copied.Data).To(HaveKey("credentials")) + g.Expect(copied.Data["credentials"]).To(Equal([]byte("aws-creds-data"))) + g.Expect(copied.Data).NotTo(HaveKey("cloud")) + g.Expect(copied.Labels["hypershift.openshift.io/etcd-backup"]).To(Equal("true")) + }) + + t.Run("When copyCredentialSecret runs and destination secret already exists, It Should reuse it", func(t *testing.T) { + g := NewWithT(t) + + existingDst := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-backup-creds-my-backup", + Namespace: "hypershift", + }, + Data: map[string][]byte{"credentials": []byte("existing-data")}, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingDst).Build() + o := &Orchestrator{log: logrus.New(), client: client} + + bsl := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + } + + dstName, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) + }) + + t.Run("When copyCredentialSecret runs and source secret lacks the BSL key, It Should return error", func(t *testing.T) { + g := NewWithT(t) + + srcSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloud-credentials", + Namespace: "openshift-adp", + }, + Data: map[string][]byte{"wrong-key": []byte("data")}, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(srcSecret).Build() + o := &Orchestrator{log: logrus.New(), client: client} + + bsl := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + } + + _, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("does not contain key")) + }) + + t.Run("When copyCredentialSecret runs without BSL credential reference, It Should return error", func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + o := &Orchestrator{log: logrus.New(), client: client} + + bsl := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{}, + } + + _, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("no credential reference")) + }) +} + +func TestSetEncryptionFields(t *testing.T) { + tests := []struct { + name string + setup func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) + assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) + }{ + { + name: "When setEncryptionFields runs with S3 storage and HostedCluster AWS KMS, It Should set S3 KMS key ARN", + setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { + return &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.S3BackupStorage, + S3: hyperv1.HCPEtcdBackupS3{}, + }, &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Etcd: hyperv1.EtcdSpec{ + Managed: &hyperv1.ManagedEtcdSpec{ + Backup: hyperv1.HCPEtcdBackupConfig{ + Platform: hyperv1.AWSBackupConfigPlatform, + AWS: hyperv1.HCPEtcdBackupConfigAWS{ + KMSKeyARN: "arn:aws:kms:us-east-1:123456789012:key/test-key", + }, + }, + }, + }, + }, + } + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.KMSKeyARN).To(Equal("arn:aws:kms:us-east-1:123456789012:key/test-key")) + }, + }, + { + name: "When setEncryptionFields runs with Azure storage and HostedCluster encryption URL, It Should set Azure encryption key URL", + setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { + return &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.AzureBlobBackupStorage, + AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}, + }, &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Etcd: hyperv1.EtcdSpec{ + Managed: &hyperv1.ManagedEtcdSpec{ + Backup: hyperv1.HCPEtcdBackupConfig{ + Platform: hyperv1.AzureBackupConfigPlatform, + Azure: hyperv1.HCPEtcdBackupConfigAzure{ + EncryptionKeyURL: "https://myvault.vault.azure.net/keys/mykey/version1", + }, + }, + }, + }, + }, + } + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.AzureBlob.EncryptionKeyURL).To(Equal("https://myvault.vault.azure.net/keys/mykey/version1")) + }, + }, + { + name: "When setEncryptionFields runs with S3 storage and no managed etcd, It Should leave S3 KMS key ARN empty", + setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { + return &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.S3BackupStorage, + S3: hyperv1.HCPEtcdBackupS3{}, + }, &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{Etcd: hyperv1.EtcdSpec{}}, + } + }, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.KMSKeyARN).To(BeEmpty()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + storage, hc := tt.setup() + setEncryptionFields(storage, hc) + tt.assert(g, storage) + }) + } +} + +func TestSetCredentialRef(t *testing.T) { + tests := []struct { + name string + storage *hyperv1.HCPEtcdBackupStorage + want string // credential name on the right branch + isAzure bool + }{ + { + name: "When setCredentialRef runs on S3 backup storage, It Should set S3 credentials reference name", + storage: &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.S3BackupStorage, + S3: hyperv1.HCPEtcdBackupS3{}, + }, + want: "my-creds", + }, + { + name: "When setCredentialRef runs on Azure blob backup storage, It Should set Azure credentials reference name", + storage: &hyperv1.HCPEtcdBackupStorage{ + StorageType: hyperv1.AzureBlobBackupStorage, + AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}, + }, + want: "my-creds", + isAzure: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + setCredentialRef(tt.storage, "my-creds") + if tt.isAzure { + g.Expect(tt.storage.AzureBlob.Credentials.Name).To(Equal(tt.want)) + } else { + g.Expect(tt.storage.S3.Credentials.Name).To(Equal(tt.want)) + } + }) + } +} + +func TestCleanupCredentialSecret(t *testing.T) { + scheme := testScheme() + + t.Run("When CleanupCredentialSecret runs and credential secret exists, It Should delete the secret", func(t *testing.T) { + g := NewWithT(t) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "etcd-backup-creds-test", Namespace: "hypershift"}, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + o := &Orchestrator{ + log: logrus.New(), + client: client, + HONamespace: "hypershift", + CredSecretName: "etcd-backup-creds-test", + } + + err := o.CleanupCredentialSecret(context.TODO()) + g.Expect(err).NotTo(HaveOccurred()) + + err = client.Get(context.TODO(), types.NamespacedName{Name: "etcd-backup-creds-test", Namespace: "hypershift"}, &corev1.Secret{}) + g.Expect(err).To(HaveOccurred()) + }) + + t.Run("When CleanupCredentialSecret runs and credential secret is already absent, It Should succeed", func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + o := &Orchestrator{ + log: logrus.New(), + client: client, + HONamespace: "hypershift", + CredSecretName: "nonexistent", + } + + err := o.CleanupCredentialSecret(context.TODO()) + g.Expect(err).NotTo(HaveOccurred()) + }) + + t.Run("When CleanupCredentialSecret runs with empty credential name, It Should succeed without error", func(t *testing.T) { + g := NewWithT(t) + o := &Orchestrator{log: logrus.New()} + err := o.CleanupCredentialSecret(context.TODO()) + g.Expect(err).NotTo(HaveOccurred()) + }) +} diff --git a/pkg/platform/aws/aws.go b/pkg/platform/aws/aws.go index ae4a9ee8e..a1f9c0e3b 100644 --- a/pkg/platform/aws/aws.go +++ b/pkg/platform/aws/aws.go @@ -1,10 +1 @@ package aws - -import ( - hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" - crclient "sigs.k8s.io/controller-runtime/pkg/client" -) - -func RestoreTasks(hcp *hyperv1.HostedControlPlane, client crclient.Client) error { - return nil -} From b61f00d5e14e4e229b147d2ed521e0919f8b66e4 Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Tue, 7 Apr 2026 22:51:57 +0200 Subject: [PATCH 3/4] docs: add HCPEtcdBackup implementation reference Document the full HCPEtcdBackup integration including architecture, backup/restore flows, configuration, credential handling, storage layout, dependency chain (PRs #8139, #8010, #8017, #8040, #8114, enhancement #1945), and troubleshooting guide. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Juan Manuel Parrilla Madrid --- .../HCPEtcdBackup-implementation.md | 533 ++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 docs/references/HCPEtcdBackup/HCPEtcdBackup-implementation.md diff --git a/docs/references/HCPEtcdBackup/HCPEtcdBackup-implementation.md b/docs/references/HCPEtcdBackup/HCPEtcdBackup-implementation.md new file mode 100644 index 000000000..8241943d4 --- /dev/null +++ b/docs/references/HCPEtcdBackup/HCPEtcdBackup-implementation.md @@ -0,0 +1,533 @@ +# HCPEtcdBackup Integration + +## Table of Contents +- [Overview](#overview) +- [Architecture](#architecture) +- [Backup Flow](#backup-flow) +- [Restore Flow](#restore-flow) +- [Configuration](#configuration) +- [Storage Layout](#storage-layout) +- [Credential Handling](#credential-handling) +- [Implementation Details](#implementation-details) +- [Dependencies](#dependencies) +- [Manual Testing](#manual-testing) +- [Troubleshooting](#troubleshooting) + +## Overview + +The HCPEtcdBackup integration adds an alternative etcd backup method to the OADP plugin. Instead of relying on CSI VolumeSnapshots or filesystem-level backups of etcd data volumes, it leverages the HyperShift Operator's `HCPEtcdBackup` controller to perform native etcd snapshots and upload them to object storage. + +### Backup Methods + +The plugin supports two mutually exclusive etcd backup methods, controlled by the `etcdBackupMethod` configuration key: + +| Method | Value | Description | +|---|---|---| +| **Volume Snapshot** | `volumeSnapshot` (default) | Uses CSI VolumeSnapshots or FSBackup to capture etcd PVCs. This is the legacy behavior. | +| **Etcd Snapshot** | `etcdSnapshot` | Creates an `HCPEtcdBackup` CR that triggers a native `etcdctl snapshot save`, then uploads the snapshot to the same object store used by Velero. | + +### Key Benefits of etcdSnapshot + +- Produces a portable, self-contained etcd snapshot (`.db` file) +- No dependency on CSI drivers or storage-class-specific snapshot mechanisms +- Snapshot is stored alongside the Velero backup data in the BSL +- The snapshot URL is persisted in the HostedCluster status, surviving CR retention policies + +## Architecture + +### Components + +```text + OADP Plugin (BackupPlugin) + │ + ┌──────────────┼──────────────┐ + │ │ │ + createEtcdBackup Execute() waitForCompletion + │ │ + ▼ ▼ + ┌─────────────────┐ ┌──────────────────┐ + │ Orchestrator │ │ Poll Condition │ + │ - fetchBSL │ │ - VerifyInProgress│ + │ - mapBSLToStorage│ │ - WaitForCompletion│ + │ - copyCredSecret│ └──────────────────┘ + │ - Create CR │ + └────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ HCPEtcdBackup CR │ (in HCP namespace) + └────────┬─────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ HyperShift Operator │ (HCPEtcdBackup controller) + │ - etcdctl snapshot │ + │ - Upload to S3/Azure│ + │ - Update HC status │ + └──────────────────────┘ +``` + +### File Layout + +| File | Purpose | +|---|---| +| `pkg/etcdbackup/orchestrator.go` | Core orchestration: BSL mapping, CR creation, polling, credential copy | +| `pkg/core/backup.go` | Backup plugin: etcd backup method routing, pod/PVC exclusion | +| `pkg/core/restore.go` | Restore plugin: snapshotURL injection into HostedCluster spec | +| `pkg/common/types.go` | Shared constants for backup methods, annotations, volume names | +| `pkg/common/scheme.go` | Scheme registration including apiextensionsv1 for CRD checks | + +## Backup Flow + +### Sequence + +1. **Plugin initialization** (`NewBackupPlugin`): Reads `etcdBackupMethod` from the ConfigMap. Validates the value. Defaults to `volumeSnapshot`. + +2. **HCP resolution**: On the first `Execute()` call, the plugin resolves the `HostedControlPlane` from the backup's included namespaces. + +3. **HCPEtcdBackup creation** (etcdSnapshot only): Runs once, idempotent across all `Execute()` calls: + - Checks that the `HCPEtcdBackup` CRD exists in the cluster (safenet) + - Fetches the Velero `BackupStorageLocation` (BSL) + - Maps BSL config to `HCPEtcdBackupStorage` (S3 or Azure Blob) + - Copies the BSL credential Secret to the HO namespace, remapping the data key from `cloud` to `credentials` + - Optionally sets encryption fields (KMS key ARN / Azure encryption key URL) from the HostedCluster spec + - Creates the `HCPEtcdBackup` CR in the HCP namespace with a unique name (`oadp-{backup-name}-{random-4-chars}`) + - Polls until the controller acknowledges the backup (InProgress or Succeeded) + +4. **Wait for completion**: When the `HostedControlPlane` or `HostedCluster` item is processed, the plugin waits for the `HCPEtcdBackup` to reach a terminal state (succeeded or failed). Timeout: 10 minutes. + +5. **Cleanup**: After completion, the copied credential Secret is deleted from the HO namespace. + +6. **Pod exclusion**: Etcd pods are excluded from the backup entirely (`return nil, nil, nil`) to prevent CSI VolumeSnapshots or FSBackup of their volumes. + +7. **PVC exclusion**: Etcd PVCs (names matching `data-etcd-*`) are excluded from the backup to prevent CSI snapshots. + +### Ordering Independence + +The `Execute()` method is called once per backed-up item, with no guaranteed ordering. The plugin handles this by: + +- Creating the `HCPEtcdBackup` CR before the switch statement (after HCP resolution), so it runs regardless of which item arrives first +- Making creation idempotent: if the orchestrator already created a CR, subsequent calls are no-ops +- Calling `waitForEtcdBackupCompletion()` in both the HCP and HC cases — the wait is also idempotent (returns immediately after the first successful wait) + +## Restore Flow + +### Sequence + +1. When the `HostedCluster` item is processed during restore, the plugin reads the etcd snapshot URL from the annotation `hypershift.openshift.io/etcd-snapshot-url`. This annotation is set during backup because Velero strips status fields from items during restore. + +2. If the URL is present and the HC has managed etcd (`spec.etcd.managed != nil`), the plugin injects the URL into `spec.etcd.managed.storage.restoreSnapshotURL`. + +3. The modified HC is written back to Velero's output, so when the HC is created in the target cluster, the HyperShift Operator uses the snapshot URL to restore etcd from the snapshot. + +### Why an Annotation Instead of Status + +Velero strips the `status` subresource from items during restore. The backup plugin also injects `lastSuccessfulEtcdBackupURL` into the HC status for observability, but the restore plugin reads the URL from the annotation `hypershift.openshift.io/etcd-snapshot-url` since that survives the restore process. + +> **Note**: The `lastSuccessfulEtcdBackupURL` status field is also set via unstructured map access during backup for informational purposes, but the restore flow relies exclusively on the annotation. + +## Configuration + +### Plugin ConfigMap + +The plugin reads its configuration from a ConfigMap named `hypershift-oadp-plugin-config` in the OADP namespace (typically `openshift-adp`). + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hypershift-oadp-plugin-config + namespace: openshift-adp +data: + etcdBackupMethod: "etcdSnapshot" # or "volumeSnapshot" (default) + hoNamespace: "hypershift" # HyperShift Operator namespace (default) + migration: "true" # Enable migration mode (optional) +``` + +### Configuration Keys + +| Key | Values | Default | Description | +|---|---|---|---| +| `etcdBackupMethod` | `volumeSnapshot`, `etcdSnapshot` | `volumeSnapshot` | Controls which etcd backup strategy is used | +| `hoNamespace` | any namespace name | `hypershift` | Namespace where the HyperShift Operator runs | +| `migration` | `true`, `false` | `false` | Enables migration-specific behavior (e.g., Agent platform PreserveOnDelete) | + +### Generating the ConfigMap + +A helper script is available at the project's documentation directory: + +```bash +# Set etcdSnapshot method +./generate-plugin-config.sh -e etcdSnapshot + +# Dry-run to review +./generate-plugin-config.sh -e etcdSnapshot -d + +# Override all defaults +./generate-plugin-config.sh -n my-adp-ns -e etcdSnapshot -o my-ho-ns -m true +``` + +### Backup Manifest + +When using `etcdSnapshot`, the Velero Backup manifest should disable volume-level backups since etcd data is handled by the HCPEtcdBackup controller: + +```yaml +apiVersion: velero.io/v1 +kind: Backup +metadata: + name: hcp-aws-backup + namespace: openshift-adp +spec: + storageLocation: default + includedNamespaces: + - clusters + - clusters- + includedResources: + - sa + - role + - rolebinding + - pod + - pvc + - pv + - configmap + - secrets + - services + - deployments + - statefulsets + - hostedcluster + - nodepool + - hostedcontrolplane + - cluster + - awscluster + - awsmachinetemplate + - awsmachine + - machinedeployment + - machineset + - machine + - route + - clusterdeployment + - namespace + snapshotMoveData: false + defaultVolumesToFsBackup: false + snapshotVolumes: false +``` + +## Storage Layout + +The etcd snapshot is stored alongside the Velero backup data in the BSL, following Velero's directory convention: + +```text +s3:////backups//etcd-backup/.db +``` + +Example: +```text +s3://my-oadp-bucket/backup-objects/backups/hcp-aws-backup/etcd-backup/1775575637.db +``` + +This ensures: +- The snapshot is co-located with the rest of the backup +- Velero does not flag `etcd-backup` as an invalid top-level directory (which would make the BSL unavailable) +- Backup retention policies applied to the Velero backup directory also cover the etcd snapshot + +## Credential Handling + +### BSL to HCPEtcdBackup Credential Flow + +The HCPEtcdBackup controller needs credentials to upload the snapshot to object storage. The OADP plugin bridges the gap between Velero's BSL credentials and the controller's expectations: + +1. **Source**: The BSL references a Secret via `spec.credential` (a `SecretKeySelector` with `name` and `key`, typically key = `cloud`) + +2. **Copy**: The plugin copies the credential data to a new Secret in the HO namespace with: + - Name: `etcd-backup-creds-` + - Label: `hypershift.openshift.io/etcd-backup: "true"` + - Key remapping: BSL key (e.g., `cloud`) is remapped to `credentials` (expected by the controller) + +3. **Reuse**: If the destination Secret already exists, it is reused (STS credentials contain an IAM Role ARN that does not rotate) + +4. **Cleanup**: After the backup completes (or fails), the copied Secret is deleted + +### Key Remapping + +The controller mounts the credential Secret as a volume at `/etc/etcd-backup-creds/` and reads the file `credentials`. Velero BSL Secrets typically store credentials under the key `cloud`. The plugin extracts only the referenced key and writes it as `credentials` in the destination Secret. + +## Implementation Details + +### CRD Existence Check + +Before creating an `HCPEtcdBackup` CR, the plugin verifies that the CRD exists in the cluster. This is a safenet — if `etcdBackupMethod` is `etcdSnapshot` but the CRD is missing, the backup fails with a clear error rather than silently falling back. + +The check requires `apiextensionsv1` to be registered in the client scheme (`pkg/common/scheme.go`). + +### Polling + +The plugin uses `wait.PollUntilContextTimeout` from `k8s.io/apimachinery/pkg/util/wait` to poll the `HCPEtcdBackup` status: + +- **VerifyInProgress**: 30-second timeout, 5-second interval. Checks that the controller acknowledged the backup. +- **WaitForCompletion**: 10-minute timeout, 5-second interval. Waits for terminal state (succeeded or failed). + +Both check the `BackupCompleted` condition on the CR. + +### Unique CR Naming + +Each backup creates an `HCPEtcdBackup` CR with a unique name: `oadp--<4-char-random-suffix>`. This uses `k8s.io/apimachinery/pkg/util/rand.String(4)` and prevents collisions with previous backup runs. + +## Dependencies + +### HyperShift PRs + +This feature depends on changes in the openshift/hypershift repository: + +| PR | Description | Jira | Status | +|---|---|---|---| +| [#8139](https://github.com/openshift/hypershift/pull/8139) | HCPEtcdBackup controller | [CNTRLPLANE-2678](https://issues.redhat.com/browse/CNTRLPLANE-2678) | Pending merge | +| CNTRLPLANE-3173 | `LastSuccessfulEtcdBackupURL` field in HostedClusterStatus | [CNTRLPLANE-3173](https://issues.redhat.com/browse/CNTRLPLANE-3173) | Pending merge | + +#### PR #8139 Dependency Chain (all merged) + +The HCPEtcdBackup controller (PR #8139) depends on the following merged PRs: + +| PR | Description | +|---|---| +| [#8010](https://github.com/openshift/hypershift/pull/8010) | `fetch-etcd-certs` CPO subcommand | +| [#8017](https://github.com/openshift/hypershift/pull/8017) | `etcd-upload` CPO subcommand | +| [#8040](https://github.com/openshift/hypershift/pull/8040) | `etcd-backup` CPO subcommand | +| [#8114](https://github.com/openshift/hypershift/pull/8114) | Transfer Manager upgrade | + +### OADP Plugin PR + +| PR | Description | Jira | +|---|---|---| +| This PR | Integrate HCPEtcdBackup lifecycle into OADP backup/restore flow | [CNTRLPLANE-2685](https://issues.redhat.com/browse/CNTRLPLANE-2685) | + +### Enhancement + +The overall design is defined in [Enhancement PR #1945](https://github.com/openshift/enhancements/pull/1945). + +### Post-Merge Vendor Update + +Once both HyperShift PRs are merged, the vendor must be updated to: + +1. Replace `getLastSuccessfulEtcdBackupURL()` unstructured helper in `pkg/core/restore.go` with direct field access: `hc.Status.LastSuccessfulEtcdBackupURL` +2. Remove local constants (`BackupInProgressReason`, `BackupRejectedReason`, `EtcdBackupSucceeded`) in `pkg/common/types.go` in favor of the API-defined constants + +## Manual Testing + +### Prerequisites + +- A management cluster with the HyperShift Operator running with the `HCPEtcdBackup` feature gate enabled +- OADP installed with a valid `BackupStorageLocation` (BSL) +- The OADP plugin image built and deployed with `etcdSnapshot` support +- A running HostedCluster + +### Testing the Backup Flow + +1. Create the plugin ConfigMap with `etcdSnapshot` method: + +```bash +cat < # typically "hypershift" +EOF +``` + +2. Create a Velero Backup targeting the HostedCluster: + +```yaml +apiVersion: velero.io/v1 +kind: Backup +metadata: + name: hcp-aws-backup + namespace: openshift-adp + labels: + velero.io/storage-location: default +spec: + storageLocation: default + csiSnapshotTimeout: 10m0s + includedNamespaces: + - + - - + includedResources: + - sa + - role + - rolebinding + - pod + - configmap + - priorityclasses + - pdb + - hostedcluster + - nodepool + - secrets + - services + - deployments + - statefulsets + - hostedcontrolplane + - cluster + - awscluster + - awsmachinetemplate + - awsmachine + - machinedeployment + - machineset + - machine + - route + - clusterdeployment + - namespace + excludedResources: [] + itemOperationTimeout: 4h0m0s + ttl: 2h0m0s + snapshotMoveData: false + defaultVolumesToFsBackup: false + snapshotVolumes: false +``` + +3. Verify the HCPEtcdBackup CR was created in the HCP namespace: + +```bash +oc get hcpetcdbackups -n - +``` + +4. Wait for the backup to complete and check the snapshot URL: + +```bash +oc get hcpetcdbackup -n - \ + -o jsonpath='{.items[0].status.snapshotURL}' +``` + +5. Verify the snapshot was uploaded to the BSL bucket: + +```bash +aws s3 ls s3:////backups/hcp-aws-backup/etcd-backup/ +``` + +6. Confirm the `lastSuccessfulEtcdBackupURL` is set on the HostedCluster status: + +```bash +oc get hostedcluster -n \ + -o jsonpath='{.status.lastSuccessfulEtcdBackupURL}' +``` + +### Testing the Restore Flow + +1. Delete the HostedCluster (or use a different management cluster) to simulate a disaster recovery scenario. + +2. Create a Velero Restore from the backup: + +```yaml +apiVersion: velero.io/v1 +kind: Restore +metadata: + name: hcp-aws-restore + namespace: openshift-adp +spec: + includedNamespaces: + - + - - + backupName: hcp-aws-backup + cleanupBeforeRestore: CleanupRestored + veleroManagedClustersBackupName: hcp-aws-backup + veleroCredentialsBackupName: hcp-aws-backup + veleroResourcesBackupName: hcp-aws-backup + restorePVs: false + preserveNodePorts: true + existingResourcePolicy: update + excludedResources: + - nodes + - events + - events.events.k8s.io + - backups.velero.io + - restores.velero.io + - resticrepositories.velero.io +``` + +3. Verify the restored HostedCluster has `restoreSnapshotURL` injected: + +```bash +oc get hostedcluster -n \ + -o jsonpath='{.spec.etcd.managed.storage.restoreSnapshotURL}' +``` + +The output should be an array containing the snapshot URL, e.g.: +``` +["s3:////backups/hcp-aws-backup/etcd-backup/1775589976.db"] +``` + +### Manual restoreSnapshotURL Injection + +To manually test the restore without going through the full OADP restore flow, you can patch the HostedCluster directly. Note that `restoreSnapshotURL` is an **array**, not a string: + +```bash +# Correct — array syntax +oc patch hostedcluster -n --type=merge \ + -p '{"spec":{"etcd":{"managed":{"storage":{"restoreSnapshotURL":["s3:////etcd-backup/snapshot.db"]}}}}}' + +# Wrong — will be rejected by the API +oc patch hostedcluster -n --type=merge \ + -p '{"spec":{"etcd":{"managed":{"storage":{"restoreSnapshotURL":"s3://..."}}}}}' +``` + +> **Important**: The `restoreSnapshotURL` field only takes effect during HostedCluster bootstrap (initial etcd creation). Patching it on an already running cluster will not trigger an etcd restore. To test a full restore, the HostedCluster must be deleted and recreated via the OADP restore flow. + +### Verifying volumeSnapshot Method is Unchanged + +To confirm the default `volumeSnapshot` method still works correctly: + +1. Remove or set `etcdBackupMethod: volumeSnapshot` in the ConfigMap +2. Run a backup and verify CSI VolumeSnapshots are created for etcd PVCs +3. Verify no `HCPEtcdBackup` CR is created + +## Troubleshooting + +### BSL Unavailable After Backup + +**Symptom**: `BackupStorageLocation "default" is unavailable: Backup store contains invalid top-level directories` + +**Cause**: An older version of the plugin stored the etcd snapshot at `/etcd-backup/` instead of inside the backup directory. + +**Fix**: Delete the orphaned directory from the bucket: +```bash +aws s3 rm s3:////etcd-backup/ --recursive +``` + +### Credential Errors (IMDS / No Credentials Found) + +**Symptom**: The etcd backup Job fails with `no EC2 IMDS role found` or similar credential errors. + +**Cause**: The credential Secret was not remapped correctly, or an old Secret (with key `cloud` instead of `credentials`) is being reused. + +**Fix**: Delete the stale credential Secret and retry: +```bash +oc delete secret -n hypershift -l hypershift.openshift.io/etcd-backup=true +``` + +### HCPEtcdBackup CRD Not Found + +**Symptom**: `etcdBackupMethod is "etcdSnapshot" but HCPEtcdBackup CRD not found in the cluster` + +**Cause**: The HyperShift Operator does not have the HCPEtcdBackup controller enabled (requires feature gate `HCPEtcdBackup`). + +**Fix**: Enable the feature gate on the HyperShift Operator or switch to `volumeSnapshot` method. + +### Backup Reuses Old HCPEtcdBackup + +**Symptom**: The backup completes instantly without creating a new etcd snapshot, reusing a previous `snapshotURL`. + +**Cause**: An old `HCPEtcdBackup` CR with a completed status still exists in the HCP namespace. Since v8, CR names include a random suffix to prevent this. + +**Fix**: Delete old CRs before running a new backup: +```bash +oc delete hcpetcdbackups --all -n +``` + +### Unknown Configuration Key Warning + +**Symptom**: Velero logs show `unknown configuration key: etcdBackupMethod with value etcdSnapshot` + +**Cause**: The plugin validator does not recognize the key. This was fixed to treat `etcdBackupMethod` and `hoNamespace` as known keys handled during plugin initialization. + +**Fix**: Ensure you are running an updated plugin image that includes this fix. From 3711f8f77fab0349999b324f31833719532e65ea Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Mon, 13 Apr 2026 09:43:33 +0200 Subject: [PATCH 4/4] test: add comprehensive unit tests for etcd backup orchestrator and validation Cover Execute paths for all item kinds, orchestrator lifecycle (CreateEtcdBackup, VerifyInProgress, WaitForCompletion), and platform validation for both backup and restore validators. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Juan Manuel Parrilla Madrid --- pkg/core/backup_test.go | 292 +++++++++ pkg/core/validation/backup_test.go | 114 +++- pkg/core/validation/restore_test.go | 134 +++++ pkg/etcdbackup/orchestrator_test.go | 877 ++++++++++++++++++++-------- 4 files changed, 1162 insertions(+), 255 deletions(-) create mode 100644 pkg/core/validation/restore_test.go diff --git a/pkg/core/backup_test.go b/pkg/core/backup_test.go index 9a8bc9592..15497a8ba 100644 --- a/pkg/core/backup_test.go +++ b/pkg/core/backup_test.go @@ -1 +1,293 @@ package core + +// Test scenario names follow: "When , It Should ". + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + common "github.com/openshift/hypershift-oadp-plugin/pkg/common" + plugtypes "github.com/openshift/hypershift-oadp-plugin/pkg/core/types" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/sirupsen/logrus" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// mockValidator implements validation.BackupValidator for testing. +type mockValidator struct { + validatePlatformErr error +} + +func (m *mockValidator) ValidatePluginConfig(_ map[string]string) (*plugtypes.BackupOptions, error) { + return &plugtypes.BackupOptions{}, nil +} + +func (m *mockValidator) ValidatePlatformConfig(_ *hyperv1.HostedControlPlane, _ *velerov1.Backup) error { + return m.validatePlatformErr +} + +func newTestBackupPlugin(objects ...runtime.Object) *BackupPlugin { + scheme := common.CustomScheme + + hcpCRD := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "hostedcontrolplanes.hypershift.openshift.io"}, + } + + hcp := &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{Name: "test-hcp", Namespace: "clusters-test"}, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{Type: hyperv1.AWSPlatform}, + }, + } + + allObjects := []runtime.Object{hcpCRD, hcp} + allObjects = append(allObjects, objects...) + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(allObjects...). + Build() + + return &BackupPlugin{ + log: logrus.New(), + ctx: context.Background(), + client: client, + config: map[string]string{}, + validator: &mockValidator{}, + hcp: hcp, + BackupOptions: &plugtypes.BackupOptions{}, + hoNamespace: "hypershift", + etcdBackupMethod: common.EtcdBackupMethodVolume, + } +} + +func newUnstructuredItem(kind, apiVersion, name, namespace string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": map[string]any{ + "name": name, + "namespace": namespace, + }, + }, + } +} + +func newTestBackup() *velerov1.Backup { + return &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup", Namespace: "openshift-adp"}, + Spec: velerov1.BackupSpec{ + IncludedNamespaces: []string{"clusters", "clusters-test"}, + }, + } +} + +func TestExecute(t *testing.T) { + falseVal := false + + tests := []struct { + name string + setup func(*BackupPlugin) + item func() *unstructured.Unstructured + backup func() *velerov1.Backup + wantNilResult bool + wantErr bool + assert func(*GomegaWithT, runtime.Unstructured, *BackupPlugin) + }{ + // HostedCluster cases + { + name: "When Execute processes a HostedCluster item, It Should add restore annotation", + item: func() *unstructured.Unstructured { + return newUnstructuredItem("HostedCluster", "hypershift.openshift.io/v1beta1", "my-hc", "clusters") + }, + backup: newTestBackup, + assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) { + metadata := result.UnstructuredContent()["metadata"].(map[string]any) + annotations := metadata["annotations"].(map[string]any) + _, exists := annotations[common.HostedClusterRestoredFromBackupAnnotation] + g.Expect(exists).To(BeTrue()) + }, + }, + { + name: "When Execute processes a HostedCluster with cached etcdSnapshotURL, It Should inject URL into status and annotation", + setup: func(bp *BackupPlugin) { + bp.etcdSnapshotURL = "s3://bucket/backups/test/etcd-backup/snapshot.db" + }, + item: func() *unstructured.Unstructured { + return newUnstructuredItem("HostedCluster", "hypershift.openshift.io/v1beta1", "my-hc", "clusters") + }, + backup: newTestBackup, + assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) { + metadata := result.UnstructuredContent()["metadata"].(map[string]any) + annotations := metadata["annotations"].(map[string]any) + g.Expect(annotations[common.EtcdSnapshotURLAnnotation]).To(Equal("s3://bucket/backups/test/etcd-backup/snapshot.db")) + + status := result.UnstructuredContent()["status"].(map[string]any) + g.Expect(status["lastSuccessfulEtcdBackupURL"]).To(Equal("s3://bucket/backups/test/etcd-backup/snapshot.db")) + }, + }, + // HostedControlPlane cases + { + name: "When Execute processes a HostedControlPlane with volumeSnapshot method, It Should not create etcd backup", + item: func() *unstructured.Unstructured { + item := newUnstructuredItem("HostedControlPlane", "hypershift.openshift.io/v1beta1", "test-hcp", "clusters-test") + item.Object["spec"] = map[string]any{ + "platform": map[string]any{"type": "AWS"}, + } + return item + }, + backup: newTestBackup, + assert: func(g *GomegaWithT, _ runtime.Unstructured, bp *BackupPlugin) { + g.Expect(bp.etcdOrchestrator).To(BeNil()) + }, + }, + // Pod cases + { + name: "When Execute processes an etcd Pod with volumeSnapshot method and fsBackup disabled, It Should add FSBackup label", + item: func() *unstructured.Unstructured { + return newUnstructuredItem("Pod", "v1", "etcd-0", "clusters-test") + }, + backup: func() *velerov1.Backup { + b := newTestBackup() + b.Spec.DefaultVolumesToFsBackup = &falseVal + return b + }, + assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) { + metadata := result.UnstructuredContent()["metadata"].(map[string]any) + labels := metadata["labels"].(map[string]any) + g.Expect(labels[common.FSBackupLabelName]).To(Equal("true")) + }, + }, + { + name: "When Execute processes an etcd Pod with etcdSnapshot method, It Should skip the pod", + setup: func(bp *BackupPlugin) { + bp.etcdBackupMethod = common.EtcdBackupMethodEtcdSnapshot + }, + item: func() *unstructured.Unstructured { + return newUnstructuredItem("Pod", "v1", "etcd-0", "clusters-test") + }, + backup: newTestBackup, + wantNilResult: true, + }, + { + name: "When Execute processes a non-etcd Pod, It Should pass through unchanged", + item: func() *unstructured.Unstructured { + return newUnstructuredItem("Pod", "v1", "kube-apiserver-0", "clusters-test") + }, + backup: newTestBackup, + assert: func(g *GomegaWithT, result runtime.Unstructured, _ *BackupPlugin) { + metadata := result.UnstructuredContent()["metadata"].(map[string]any) + g.Expect(metadata["name"]).To(Equal("kube-apiserver-0")) + }, + }, + // PVC cases + { + name: "When Execute processes an etcd PVC with etcdSnapshot method, It Should skip the PVC", + setup: func(bp *BackupPlugin) { + bp.etcdBackupMethod = common.EtcdBackupMethodEtcdSnapshot + }, + item: func() *unstructured.Unstructured { + return newUnstructuredItem("PersistentVolumeClaim", "v1", "data-etcd-0", "clusters-test") + }, + backup: newTestBackup, + wantNilResult: true, + }, + { + name: "When Execute processes a PVC with kubevirt RHCOS label, It Should skip the PVC", + item: func() *unstructured.Unstructured { + item := newUnstructuredItem("PersistentVolumeClaim", "v1", "rhcos-disk", "clusters-test") + item.Object["metadata"].(map[string]any)["labels"] = map[string]any{ + common.KubevirtRHCOSLabel: "true", + } + return item + }, + backup: newTestBackup, + wantNilResult: true, + }, + { + name: "When Execute processes a regular PVC, It Should pass through unchanged", + item: func() *unstructured.Unstructured { + return newUnstructuredItem("PersistentVolumeClaim", "v1", "some-data", "clusters-test") + }, + backup: newTestBackup, + }, + // DataVolume cases + { + name: "When Execute processes a DataVolume with kubevirt RHCOS label, It Should skip it", + item: func() *unstructured.Unstructured { + item := newUnstructuredItem("DataVolume", "cdi.kubevirt.io/v1beta1", "rhcos-dv", "clusters-test") + item.Object["metadata"].(map[string]any)["labels"] = map[string]any{ + common.KubevirtRHCOSLabel: "true", + } + return item + }, + backup: newTestBackup, + wantNilResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + bp := newTestBackupPlugin() + if tt.setup != nil { + tt.setup(bp) + } + + result, _, err := bp.Execute(tt.item(), tt.backup()) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + if tt.wantNilResult { + g.Expect(result).To(BeNil()) + return + } + + g.Expect(result).NotTo(BeNil()) + if tt.assert != nil { + tt.assert(g, result, bp) + } + }) + } +} + +func TestWaitForEtcdBackupCompletion(t *testing.T) { + tests := []struct { + name string + setup func(*BackupPlugin) + }{ + { + name: "When orchestrator is nil, It Should return nil", + setup: func(bp *BackupPlugin) { + bp.etcdOrchestrator = nil + }, + }, + { + name: "When snapshotURL is already cached, It Should return nil without polling", + setup: func(bp *BackupPlugin) { + bp.etcdSnapshotURL = "s3://cached-url" + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + bp := newTestBackupPlugin() + tt.setup(bp) + + err := bp.waitForEtcdBackupCompletion(context.TODO()) + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} diff --git a/pkg/core/validation/backup_test.go b/pkg/core/validation/backup_test.go index b4982aecf..e648bfdbf 100644 --- a/pkg/core/validation/backup_test.go +++ b/pkg/core/validation/backup_test.go @@ -1,57 +1,121 @@ package validation +// Test scenario names follow: "When , It Should ". + import ( "testing" . "github.com/onsi/gomega" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/sirupsen/logrus" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestValidatePluginConfig(t *testing.T) { - g := NewWithT(t) - +func TestBackupValidatePluginConfig(t *testing.T) { tests := []struct { name string config map[string]string + wantMigr bool expectError bool }{ { - name: "empty config", - config: map[string]string{}, - expectError: false, + name: "When config is empty, It Should return default options without error", + config: map[string]string{}, }, { - name: "valid config with migration", - config: map[string]string{ - "migration": "true", - }, - expectError: false, + name: "When config has migration true, It Should set Migration to true", + config: map[string]string{"migration": "true"}, + wantMigr: true, }, { - name: "When config contains etcdBackupMethod, It Should accept it without error", - config: map[string]string{ - "etcdBackupMethod": "etcdSnapshot", - }, - expectError: false, + name: "When config contains etcdBackupMethod, It Should accept it without error", + config: map[string]string{"etcdBackupMethod": "etcdSnapshot"}, }, { - name: "When config contains hoNamespace, It Should accept it without error", - config: map[string]string{ - "hoNamespace": "my-hypershift", - }, - expectError: false, + name: "When config contains hoNamespace, It Should accept it without error", + config: map[string]string{"hoNamespace": "my-hypershift"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := &BackupPluginValidator{ - Log: logrus.New(), - } + g := NewWithT(t) + p := &BackupPluginValidator{Log: logrus.New()} - _, err := p.ValidatePluginConfig(tt.config) + opts, err := p.ValidatePluginConfig(tt.config) if tt.expectError { g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(opts.Migration).To(Equal(tt.wantMigr)) + } + }) + } +} + +func TestBackupValidatePlatformConfig(t *testing.T) { + tests := []struct { + name string + platformType hyperv1.PlatformType + wantErr bool + errSubstr string + }{ + { + name: "When ValidatePlatformConfig runs with AWS platform, It Should return no error", + platformType: hyperv1.AWSPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Azure platform, It Should return no error", + platformType: hyperv1.AzurePlatform, + }, + { + name: "When ValidatePlatformConfig runs with IBMCloud platform, It Should return no error", + platformType: hyperv1.IBMCloudPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Kubevirt platform, It Should return no error", + platformType: hyperv1.KubevirtPlatform, + }, + { + name: "When ValidatePlatformConfig runs with OpenStack platform, It Should return no error", + platformType: hyperv1.OpenStackPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Agent platform, It Should return no error", + platformType: hyperv1.AgentPlatform, + }, + { + name: "When ValidatePlatformConfig runs with None platform, It Should return no error", + platformType: hyperv1.NonePlatform, + }, + { + name: "When ValidatePlatformConfig runs with unsupported platform, It Should return error", + platformType: hyperv1.PlatformType("UnsupportedPlatform"), + wantErr: true, + errSubstr: "unsupported platform type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + p := &BackupPluginValidator{Log: logrus.New()} + + hcp := &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{Name: "test-hcp", Namespace: "test-ns"}, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{Type: tt.platformType}, + }, + } + backup := &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup"}, + } + + err := p.ValidatePlatformConfig(hcp, backup) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) } else { g.Expect(err).ToNot(HaveOccurred()) } diff --git a/pkg/core/validation/restore_test.go b/pkg/core/validation/restore_test.go new file mode 100644 index 000000000..9b6165ecb --- /dev/null +++ b/pkg/core/validation/restore_test.go @@ -0,0 +1,134 @@ +package validation + +// Test scenario names follow: "When , It Should ". + +import ( + "testing" + + . "github.com/onsi/gomega" + hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestRestoreValidatePluginConfig(t *testing.T) { + tests := []struct { + name string + config map[string]string + wantMigr bool + expectError bool + }{ + { + name: "When config is empty, It Should return default options without error", + config: map[string]string{}, + }, + { + name: "When config has migration true, It Should set Migration to true", + config: map[string]string{"migration": "true"}, + wantMigr: true, + }, + { + name: "When config has migration false, It Should leave Migration as false", + config: map[string]string{"migration": "false"}, + }, + { + name: "When config has etcdBackupMethod, It Should accept it without error", + config: map[string]string{"etcdBackupMethod": "etcdSnapshot"}, + }, + { + name: "When config has hoNamespace, It Should accept it without error", + config: map[string]string{"hoNamespace": "my-hypershift"}, + }, + { + name: "When config has unknown key, It Should not return error", + config: map[string]string{"unknownKey": "value"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + p := &RestorePluginValidator{ + Log: logrus.New(), + LogHeader: "test", + } + + opts, err := p.ValidatePluginConfig(tt.config) + if tt.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(opts.Migration).To(Equal(tt.wantMigr)) + } + }) + } +} + +func TestRestoreValidatePlatformConfig(t *testing.T) { + tests := []struct { + name string + platformType hyperv1.PlatformType + wantErr bool + errSubstr string + }{ + { + name: "When ValidatePlatformConfig runs with AWS platform, It Should return no error", + platformType: hyperv1.AWSPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Azure platform, It Should return no error", + platformType: hyperv1.AzurePlatform, + }, + { + name: "When ValidatePlatformConfig runs with IBMCloud platform, It Should return no error", + platformType: hyperv1.IBMCloudPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Kubevirt platform, It Should return no error", + platformType: hyperv1.KubevirtPlatform, + }, + { + name: "When ValidatePlatformConfig runs with OpenStack platform, It Should return no error", + platformType: hyperv1.OpenStackPlatform, + }, + { + name: "When ValidatePlatformConfig runs with Agent platform, It Should return no error", + platformType: hyperv1.AgentPlatform, + }, + { + name: "When ValidatePlatformConfig runs with None platform, It Should return no error", + platformType: hyperv1.NonePlatform, + }, + { + name: "When ValidatePlatformConfig runs with unsupported platform, It Should return error", + platformType: hyperv1.PlatformType("UnsupportedPlatform"), + wantErr: true, + errSubstr: "unsupported platform type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + p := &RestorePluginValidator{ + Log: logrus.New(), + LogHeader: "test", + } + + hcp := &hyperv1.HostedControlPlane{ + ObjectMeta: metav1.ObjectMeta{Name: "test-hcp", Namespace: "test-ns"}, + Spec: hyperv1.HostedControlPlaneSpec{ + Platform: hyperv1.PlatformSpec{Type: tt.platformType}, + }, + } + + err := p.ValidatePlatformConfig(hcp, map[string]string{}) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} diff --git a/pkg/etcdbackup/orchestrator_test.go b/pkg/etcdbackup/orchestrator_test.go index 39f4818e4..36f5ec0a8 100644 --- a/pkg/etcdbackup/orchestrator_test.go +++ b/pkg/etcdbackup/orchestrator_test.go @@ -4,16 +4,20 @@ package etcdbackup import ( "context" + "fmt" "testing" + "time" . "github.com/onsi/gomega" hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/sirupsen/logrus" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + crclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -25,18 +29,16 @@ func testScheme() *runtime.Scheme { return s } -// mapBSLCase describes one row in TestMapBSLToStorage: either mapBSLToStorage (full path -// including key-prefix resolution) or a direct call to mapAWSBSLToStorage / mapAzureBSLToStorage. -type mapBSLCase struct { - name string - how mapBSLCaseHow // mapBSL | mapAWS | mapAzure - bsl *velerov1.BackupStorageLocation - mapperPrefix string // second arg to mapAWS/mapAzure; ignored for mapBSL - wantErr bool - errSubstr string - assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) +func testClient(scheme *runtime.Scheme, objs ...crclient.Object) crclient.Client { + b := fake.NewClientBuilder().WithScheme(scheme) + if len(objs) > 0 { + b = b.WithObjects(objs...).WithStatusSubresource(objs...) + } + return b.Build() } +// mapBSLCase describes one row in TestMapBSLToStorage: either mapBSLToStorage (full path +// including key-prefix resolution) or a direct call to mapAWSBSLToStorage / mapAzureBSLToStorage. type mapBSLCaseHow int const ( @@ -49,7 +51,15 @@ const ( // mapAWSBSLToStorage / mapAzureBSLToStorage in one table. func TestMapBSLToStorage(t *testing.T) { o := &Orchestrator{log: logrus.New()} - tests := []mapBSLCase{ + tests := []struct { + name string + how mapBSLCaseHow + bsl *velerov1.BackupStorageLocation + mapperPrefix string + wantErr bool + errSubstr string + assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) + }{ { name: "When mapBSLToStorage runs for AWS BSL with bucket and object storage prefix, It Should set S3 bucket region and key prefix", how: mapViaBSL, @@ -57,10 +67,7 @@ func TestMapBSLToStorage(t *testing.T) { Spec: velerov1.BackupStorageLocationSpec{ Provider: "aws", StorageType: velerov1.StorageType{ - ObjectStorage: &velerov1.ObjectStorageLocation{ - Bucket: "my-bucket", - Prefix: "velero-backups", - }, + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-bucket", Prefix: "velero-backups"}, }, Config: map[string]string{"region": "us-east-1"}, }, @@ -79,10 +86,7 @@ func TestMapBSLToStorage(t *testing.T) { Spec: velerov1.BackupStorageLocationSpec{ Provider: "azure", StorageType: velerov1.StorageType{ - ObjectStorage: &velerov1.ObjectStorageLocation{ - Bucket: "my-container", - Prefix: "velero", - }, + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-container", Prefix: "velero"}, }, Config: map[string]string{"storageAccount": "mystorageaccount"}, }, @@ -126,10 +130,7 @@ func TestMapBSLToStorage(t *testing.T) { Spec: velerov1.BackupStorageLocationSpec{ Provider: "velero.io/aws", StorageType: velerov1.StorageType{ - ObjectStorage: &velerov1.ObjectStorageLocation{ - Bucket: "my-bucket", - Prefix: "my-prefix", - }, + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-bucket", Prefix: "my-prefix"}, }, Config: map[string]string{"region": "us-west-2"}, }, @@ -170,7 +171,6 @@ func TestMapBSLToStorage(t *testing.T) { assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { g.Expect(s.S3.Bucket).To(BeEmpty()) g.Expect(s.S3.Region).To(Equal("us-east-1")) - g.Expect(s.S3.KeyPrefix).To(Equal("etcd-backup")) }, }, { @@ -268,183 +268,163 @@ func TestMapBSLToStorage(t *testing.T) { func TestCopyCredentialSecret(t *testing.T) { scheme := testScheme() - t.Run("When copyCredentialSecret runs with BSL credential key cloud, It Should remap to credentials key in destination", func(t *testing.T) { - g := NewWithT(t) - - srcSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cloud-credentials", - Namespace: "openshift-adp", + tests := []struct { + name string + objects []crclient.Object + bsl *velerov1.BackupStorageLocation + wantErr bool + errSubstr string + assert func(*GomegaWithT, string, crclient.Client) + }{ + { + name: "When copyCredentialSecret runs with BSL credential key cloud, It Should remap to credentials key in destination", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "cloud-credentials", Namespace: "openshift-adp"}, + Data: map[string][]byte{"cloud": []byte("aws-creds-data")}, + }, }, - Data: map[string][]byte{"cloud": []byte("aws-creds-data")}, - } - - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(srcSecret).Build() - o := &Orchestrator{log: logrus.New(), client: client} - - bsl := &velerov1.BackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{Name: "default"}, - Spec: velerov1.BackupStorageLocationSpec{ - Credential: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, - Key: "cloud", + bsl: &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, }, }, - } - - dstName, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) - - copied := &corev1.Secret{} - err = client.Get(context.TODO(), types.NamespacedName{Name: dstName, Namespace: "hypershift"}, copied) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(copied.Data).To(HaveKey("credentials")) - g.Expect(copied.Data["credentials"]).To(Equal([]byte("aws-creds-data"))) - g.Expect(copied.Data).NotTo(HaveKey("cloud")) - g.Expect(copied.Labels["hypershift.openshift.io/etcd-backup"]).To(Equal("true")) - }) - - t.Run("When copyCredentialSecret runs and destination secret already exists, It Should reuse it", func(t *testing.T) { - g := NewWithT(t) - - existingDst := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "etcd-backup-creds-my-backup", - Namespace: "hypershift", - }, - Data: map[string][]byte{"credentials": []byte("existing-data")}, - } - - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingDst).Build() - o := &Orchestrator{log: logrus.New(), client: client} - - bsl := &velerov1.BackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{Name: "default"}, - Spec: velerov1.BackupStorageLocationSpec{ - Credential: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, - Key: "cloud", + assert: func(g *GomegaWithT, dstName string, client crclient.Client) { + g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) + copied := &corev1.Secret{} + err := client.Get(context.TODO(), types.NamespacedName{Name: dstName, Namespace: "hypershift"}, copied) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(copied.Data).To(HaveKey("credentials")) + g.Expect(copied.Data["credentials"]).To(Equal([]byte("aws-creds-data"))) + g.Expect(copied.Data).NotTo(HaveKey("cloud")) + g.Expect(copied.Labels["hypershift.openshift.io/etcd-backup"]).To(Equal("true")) + }, + }, + { + name: "When copyCredentialSecret runs and destination secret already exists, It Should reuse it", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "etcd-backup-creds-my-backup", Namespace: "hypershift"}, + Data: map[string][]byte{"credentials": []byte("existing-data")}, }, }, - } - - dstName, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) - }) - - t.Run("When copyCredentialSecret runs and source secret lacks the BSL key, It Should return error", func(t *testing.T) { - g := NewWithT(t) - - srcSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cloud-credentials", - Namespace: "openshift-adp", + bsl: &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, }, - Data: map[string][]byte{"wrong-key": []byte("data")}, - } - - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(srcSecret).Build() - o := &Orchestrator{log: logrus.New(), client: client} - - bsl := &velerov1.BackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{Name: "default"}, - Spec: velerov1.BackupStorageLocationSpec{ - Credential: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, - Key: "cloud", + assert: func(g *GomegaWithT, dstName string, _ crclient.Client) { + g.Expect(dstName).To(Equal("etcd-backup-creds-my-backup")) + }, + }, + { + name: "When copyCredentialSecret runs and source secret lacks the BSL key, It Should return error", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "cloud-credentials", Namespace: "openshift-adp"}, + Data: map[string][]byte{"wrong-key": []byte("data")}, }, }, - } - - _, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("does not contain key")) - }) - - t.Run("When copyCredentialSecret runs without BSL credential reference, It Should return error", func(t *testing.T) { - g := NewWithT(t) - client := fake.NewClientBuilder().WithScheme(scheme).Build() - o := &Orchestrator{log: logrus.New(), client: client} + bsl: &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{ + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + }, + wantErr: true, + errSubstr: "does not contain key", + }, + { + name: "When copyCredentialSecret runs without BSL credential reference, It Should return error", + bsl: &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + Spec: velerov1.BackupStorageLocationSpec{}, + }, + wantErr: true, + errSubstr: "no credential reference", + }, + } - bsl := &velerov1.BackupStorageLocation{ - ObjectMeta: metav1.ObjectMeta{Name: "default"}, - Spec: velerov1.BackupStorageLocationSpec{}, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := testClient(scheme, tt.objects...) + o := &Orchestrator{log: logrus.New(), client: client} - _, err := o.copyCredentialSecret(context.TODO(), bsl, "openshift-adp", "hypershift", "my-backup") - g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring("no credential reference")) - }) + dstName, err := o.copyCredentialSecret(context.TODO(), tt.bsl, "openshift-adp", "hypershift", "my-backup") + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + if tt.assert != nil { + tt.assert(g, dstName, client) + } + }) + } } func TestSetEncryptionFields(t *testing.T) { tests := []struct { - name string - setup func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) - assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) + name string + storage *hyperv1.HCPEtcdBackupStorage + hc *hyperv1.HostedCluster + assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) }{ { - name: "When setEncryptionFields runs with S3 storage and HostedCluster AWS KMS, It Should set S3 KMS key ARN", - setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { - return &hyperv1.HCPEtcdBackupStorage{ - StorageType: hyperv1.S3BackupStorage, - S3: hyperv1.HCPEtcdBackupS3{}, - }, &hyperv1.HostedCluster{ - Spec: hyperv1.HostedClusterSpec{ - Etcd: hyperv1.EtcdSpec{ - Managed: &hyperv1.ManagedEtcdSpec{ - Backup: hyperv1.HCPEtcdBackupConfig{ - Platform: hyperv1.AWSBackupConfigPlatform, - AWS: hyperv1.HCPEtcdBackupConfigAWS{ - KMSKeyARN: "arn:aws:kms:us-east-1:123456789012:key/test-key", - }, - }, - }, + name: "When setEncryptionFields runs with S3 storage and HostedCluster AWS KMS, It Should set S3 KMS key ARN", + storage: &hyperv1.HCPEtcdBackupStorage{StorageType: hyperv1.S3BackupStorage, S3: hyperv1.HCPEtcdBackupS3{}}, + hc: &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Etcd: hyperv1.EtcdSpec{ + Managed: &hyperv1.ManagedEtcdSpec{ + Backup: hyperv1.HCPEtcdBackupConfig{ + Platform: hyperv1.AWSBackupConfigPlatform, + AWS: hyperv1.HCPEtcdBackupConfigAWS{KMSKeyARN: "arn:aws:kms:us-east-1:123456789012:key/test-key"}, }, }, - } + }, + }, }, assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { g.Expect(s.S3.KMSKeyARN).To(Equal("arn:aws:kms:us-east-1:123456789012:key/test-key")) }, }, { - name: "When setEncryptionFields runs with Azure storage and HostedCluster encryption URL, It Should set Azure encryption key URL", - setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { - return &hyperv1.HCPEtcdBackupStorage{ - StorageType: hyperv1.AzureBlobBackupStorage, - AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}, - }, &hyperv1.HostedCluster{ - Spec: hyperv1.HostedClusterSpec{ - Etcd: hyperv1.EtcdSpec{ - Managed: &hyperv1.ManagedEtcdSpec{ - Backup: hyperv1.HCPEtcdBackupConfig{ - Platform: hyperv1.AzureBackupConfigPlatform, - Azure: hyperv1.HCPEtcdBackupConfigAzure{ - EncryptionKeyURL: "https://myvault.vault.azure.net/keys/mykey/version1", - }, - }, - }, + name: "When setEncryptionFields runs with Azure storage and HostedCluster encryption URL, It Should set Azure encryption key URL", + storage: &hyperv1.HCPEtcdBackupStorage{StorageType: hyperv1.AzureBlobBackupStorage, AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}}, + hc: &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Etcd: hyperv1.EtcdSpec{ + Managed: &hyperv1.ManagedEtcdSpec{ + Backup: hyperv1.HCPEtcdBackupConfig{ + Platform: hyperv1.AzureBackupConfigPlatform, + Azure: hyperv1.HCPEtcdBackupConfigAzure{EncryptionKeyURL: "https://myvault.vault.azure.net/keys/mykey/version1"}, }, }, - } + }, + }, }, assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { g.Expect(s.AzureBlob.EncryptionKeyURL).To(Equal("https://myvault.vault.azure.net/keys/mykey/version1")) }, }, { - name: "When setEncryptionFields runs with S3 storage and no managed etcd, It Should leave S3 KMS key ARN empty", - setup: func() (*hyperv1.HCPEtcdBackupStorage, *hyperv1.HostedCluster) { - return &hyperv1.HCPEtcdBackupStorage{ - StorageType: hyperv1.S3BackupStorage, - S3: hyperv1.HCPEtcdBackupS3{}, - }, &hyperv1.HostedCluster{ - Spec: hyperv1.HostedClusterSpec{Etcd: hyperv1.EtcdSpec{}}, - } - }, + name: "When setEncryptionFields runs with S3 storage and no managed etcd, It Should leave S3 KMS key ARN empty", + storage: &hyperv1.HCPEtcdBackupStorage{StorageType: hyperv1.S3BackupStorage, S3: hyperv1.HCPEtcdBackupS3{}}, + hc: &hyperv1.HostedCluster{Spec: hyperv1.HostedClusterSpec{Etcd: hyperv1.EtcdSpec{}}}, assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { g.Expect(s.S3.KMSKeyARN).To(BeEmpty()) }, @@ -453,9 +433,8 @@ func TestSetEncryptionFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - storage, hc := tt.setup() - setEncryptionFields(storage, hc) - tt.assert(g, storage) + setEncryptionFields(tt.storage, tt.hc) + tt.assert(g, tt.storage) }) } } @@ -464,36 +443,28 @@ func TestSetCredentialRef(t *testing.T) { tests := []struct { name string storage *hyperv1.HCPEtcdBackupStorage - want string // credential name on the right branch - isAzure bool + assert func(*GomegaWithT, *hyperv1.HCPEtcdBackupStorage) }{ { - name: "When setCredentialRef runs on S3 backup storage, It Should set S3 credentials reference name", - storage: &hyperv1.HCPEtcdBackupStorage{ - StorageType: hyperv1.S3BackupStorage, - S3: hyperv1.HCPEtcdBackupS3{}, + name: "When setCredentialRef runs on S3 backup storage, It Should set S3 credentials reference name", + storage: &hyperv1.HCPEtcdBackupStorage{StorageType: hyperv1.S3BackupStorage, S3: hyperv1.HCPEtcdBackupS3{}}, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.S3.Credentials.Name).To(Equal("my-creds")) }, - want: "my-creds", }, { - name: "When setCredentialRef runs on Azure blob backup storage, It Should set Azure credentials reference name", - storage: &hyperv1.HCPEtcdBackupStorage{ - StorageType: hyperv1.AzureBlobBackupStorage, - AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}, + name: "When setCredentialRef runs on Azure blob backup storage, It Should set Azure credentials reference name", + storage: &hyperv1.HCPEtcdBackupStorage{StorageType: hyperv1.AzureBlobBackupStorage, AzureBlob: hyperv1.HCPEtcdBackupAzureBlob{}}, + assert: func(g *GomegaWithT, s *hyperv1.HCPEtcdBackupStorage) { + g.Expect(s.AzureBlob.Credentials.Name).To(Equal("my-creds")) }, - want: "my-creds", - isAzure: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) setCredentialRef(tt.storage, "my-creds") - if tt.isAzure { - g.Expect(tt.storage.AzureBlob.Credentials.Name).To(Equal(tt.want)) - } else { - g.Expect(tt.storage.S3.Credentials.Name).To(Equal(tt.want)) - } + tt.assert(g, tt.storage) }) } } @@ -501,44 +472,490 @@ func TestSetCredentialRef(t *testing.T) { func TestCleanupCredentialSecret(t *testing.T) { scheme := testScheme() - t.Run("When CleanupCredentialSecret runs and credential secret exists, It Should delete the secret", func(t *testing.T) { - g := NewWithT(t) - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "etcd-backup-creds-test", Namespace: "hypershift"}, - } - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() - o := &Orchestrator{ - log: logrus.New(), - client: client, - HONamespace: "hypershift", - CredSecretName: "etcd-backup-creds-test", - } - - err := o.CleanupCredentialSecret(context.TODO()) - g.Expect(err).NotTo(HaveOccurred()) - - err = client.Get(context.TODO(), types.NamespacedName{Name: "etcd-backup-creds-test", Namespace: "hypershift"}, &corev1.Secret{}) - g.Expect(err).To(HaveOccurred()) - }) - - t.Run("When CleanupCredentialSecret runs and credential secret is already absent, It Should succeed", func(t *testing.T) { - g := NewWithT(t) - client := fake.NewClientBuilder().WithScheme(scheme).Build() - o := &Orchestrator{ - log: logrus.New(), - client: client, - HONamespace: "hypershift", - CredSecretName: "nonexistent", - } - - err := o.CleanupCredentialSecret(context.TODO()) - g.Expect(err).NotTo(HaveOccurred()) - }) - - t.Run("When CleanupCredentialSecret runs with empty credential name, It Should succeed without error", func(t *testing.T) { - g := NewWithT(t) - o := &Orchestrator{log: logrus.New()} - err := o.CleanupCredentialSecret(context.TODO()) - g.Expect(err).NotTo(HaveOccurred()) - }) + tests := []struct { + name string + objects []crclient.Object + credSecretName string + assert func(*GomegaWithT, crclient.Client) + }{ + { + name: "When CleanupCredentialSecret runs and credential secret exists, It Should delete the secret", + objects: []crclient.Object{ + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "etcd-backup-creds-test", Namespace: "hypershift"}}, + }, + credSecretName: "etcd-backup-creds-test", + assert: func(g *GomegaWithT, client crclient.Client) { + err := client.Get(context.TODO(), types.NamespacedName{Name: "etcd-backup-creds-test", Namespace: "hypershift"}, &corev1.Secret{}) + g.Expect(err).To(HaveOccurred()) + }, + }, + { + name: "When CleanupCredentialSecret runs and credential secret is already absent, It Should succeed", + credSecretName: "nonexistent", + }, + { + name: "When CleanupCredentialSecret runs with empty credential name, It Should succeed without error", + credSecretName: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := testClient(scheme, tt.objects...) + o := &Orchestrator{ + log: logrus.New(), + client: client, + HONamespace: "hypershift", + CredSecretName: tt.credSecretName, + } + + err := o.CleanupCredentialSecret(context.TODO()) + g.Expect(err).NotTo(HaveOccurred()) + if tt.assert != nil { + tt.assert(g, client) + } + }) + } +} + +func TestNewOrchestrator(t *testing.T) { + g := NewWithT(t) + client := testClient(testScheme()) + o := NewOrchestrator(logrus.New(), client, "hypershift", "openshift-adp") + + g.Expect(o.HONamespace).To(Equal("hypershift")) + g.Expect(o.OADPNamespace).To(Equal("openshift-adp")) + g.Expect(o.BackupName).To(BeEmpty()) + g.Expect(o.BackupNamespace).To(BeEmpty()) + g.Expect(o.CredSecretName).To(BeEmpty()) +} + +func TestIsCreated(t *testing.T) { + tests := []struct { + name string + backupName string + want bool + }{ + { + name: "When BackupName is empty, It Should return false", + backupName: "", + want: false, + }, + { + name: "When BackupName is set, It Should return true", + backupName: "my-backup", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + o := &Orchestrator{BackupName: tt.backupName} + g.Expect(o.IsCreated()).To(Equal(tt.want)) + }) + } +} + +func TestFetchBSL(t *testing.T) { + scheme := testScheme() + + tests := []struct { + name string + objects []crclient.Object + bslName string + wantErr bool + assert func(*GomegaWithT, *velerov1.BackupStorageLocation) + }{ + { + name: "When BSL exists, It Should return it", + objects: []crclient.Object{ + &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "openshift-adp"}, + Spec: velerov1.BackupStorageLocationSpec{Provider: "aws"}, + }, + }, + bslName: "default", + assert: func(g *GomegaWithT, bsl *velerov1.BackupStorageLocation) { + g.Expect(bsl.Name).To(Equal("default")) + g.Expect(bsl.Spec.Provider).To(Equal("aws")) + }, + }, + { + name: "When BSL does not exist, It Should return error", + bslName: "nonexistent", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := testClient(scheme, tt.objects...) + o := &Orchestrator{log: logrus.New(), client: client} + + result, err := o.fetchBSL(context.TODO(), tt.bslName, "openshift-adp") + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + tt.assert(g, result) + }) + } +} + +func TestCreateEtcdBackup(t *testing.T) { + scheme := testScheme() + + tests := []struct { + name string + objects []crclient.Object + backup *velerov1.Backup + hc *hyperv1.HostedCluster + wantErr bool + errSubstr string + assert func(*GomegaWithT, *Orchestrator) + }{ + { + name: "When CreateEtcdBackup runs with valid AWS BSL, It Should create HCPEtcdBackup CR", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "cloud-credentials", Namespace: "openshift-adp"}, + Data: map[string][]byte{"cloud": []byte("aws-creds")}, + }, + &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "openshift-adp"}, + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "my-bucket", Prefix: "velero"}, + }, + Config: map[string]string{"region": "us-east-1"}, + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + }, + }, + backup: &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup", Namespace: "openshift-adp"}, + Spec: velerov1.BackupSpec{StorageLocation: "default"}, + }, + hc: &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{ + Etcd: hyperv1.EtcdSpec{ + Managed: &hyperv1.ManagedEtcdSpec{ + Backup: hyperv1.HCPEtcdBackupConfig{ + Platform: hyperv1.AWSBackupConfigPlatform, + AWS: hyperv1.HCPEtcdBackupConfigAWS{KMSKeyARN: "arn:aws:kms:us-east-1:123:key/k1"}, + }, + }, + }, + }, + }, + assert: func(g *GomegaWithT, o *Orchestrator) { + g.Expect(o.BackupName).NotTo(BeEmpty()) + g.Expect(o.BackupNamespace).To(Equal("clusters-test")) + g.Expect(o.CredSecretName).NotTo(BeEmpty()) + }, + }, + { + name: "When CreateEtcdBackup runs with nil HostedCluster, It Should succeed without encryption", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "cloud-credentials", Namespace: "openshift-adp"}, + Data: map[string][]byte{"cloud": []byte("aws-creds")}, + }, + &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "openshift-adp"}, + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{Bucket: "b"}, + }, + Config: map[string]string{"region": "eu-west-1"}, + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cloud-credentials"}, + Key: "cloud", + }, + }, + }, + }, + backup: &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup"}, + Spec: velerov1.BackupSpec{StorageLocation: "default"}, + }, + assert: func(g *GomegaWithT, o *Orchestrator) { + g.Expect(o.IsCreated()).To(BeTrue()) + }, + }, + { + name: "When CreateEtcdBackup runs and BSL not found, It Should return error", + backup: &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup"}, + Spec: velerov1.BackupSpec{StorageLocation: "nonexistent"}, + }, + wantErr: true, + errSubstr: "failed to fetch BackupStorageLocation", + }, + { + name: "When CreateEtcdBackup runs with unsupported provider, It Should return error", + objects: []crclient.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "creds", Namespace: "openshift-adp"}, + Data: map[string][]byte{"cloud": []byte("data")}, + }, + &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "openshift-adp"}, + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "gcp", + Credential: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "creds"}, + Key: "cloud", + }, + }, + }, + }, + backup: &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-backup"}, + Spec: velerov1.BackupSpec{StorageLocation: "default"}, + }, + wantErr: true, + errSubstr: "failed to map BSL", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := testClient(scheme, tt.objects...) + o := NewOrchestrator(logrus.New(), client, "hypershift", "openshift-adp") + + err := o.CreateEtcdBackup(context.TODO(), tt.backup, "clusters-test", tt.hc) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + if tt.assert != nil { + tt.assert(g, o) + } + }) + } +} + +func TestPollCondition(t *testing.T) { + scheme := testScheme() + + tests := []struct { + name string + reason string + status metav1.ConditionStatus + message string + check func(*metav1.Condition) (bool, error) + wantErr bool + errSubstr string + }{ + { + name: "When condition is already satisfied, It Should return immediately", + reason: hyperv1.BackupSucceededReason, + status: metav1.ConditionTrue, + check: func(cond *metav1.Condition) (bool, error) { + return cond != nil && cond.Reason == hyperv1.BackupSucceededReason, nil + }, + }, + { + name: "When check returns error, It Should propagate the error", + reason: hyperv1.BackupFailedReason, + status: metav1.ConditionFalse, + message: "etcd snapshot failed", + check: func(cond *metav1.Condition) (bool, error) { + if cond != nil && cond.Reason == hyperv1.BackupFailedReason { + return false, fmt.Errorf("backup failed: %s", cond.Message) + } + return false, nil + }, + wantErr: true, + errSubstr: "backup failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + eb := &hyperv1.HCPEtcdBackup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-eb", Namespace: "clusters-test"}, + } + meta.SetStatusCondition(&eb.Status.Conditions, metav1.Condition{ + Type: string(hyperv1.BackupCompleted), + Status: tt.status, + Reason: tt.reason, + Message: tt.message, + }) + + client := testClient(scheme, eb) + o := &Orchestrator{ + log: logrus.New(), + client: client, + BackupName: "test-eb", + BackupNamespace: "clusters-test", + } + + err := o.pollCondition(context.TODO(), 5*time.Second, tt.check) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func TestVerifyInProgress(t *testing.T) { + scheme := testScheme() + + tests := []struct { + name string + reason string + wantErr bool + errSubstr string + }{ + { + name: "When HCPEtcdBackup is InProgress, It Should return nil", + reason: hyperv1.BackupInProgressReason, + }, + { + name: "When HCPEtcdBackup already succeeded, It Should return nil", + reason: hyperv1.BackupSucceededReason, + }, + { + name: "When HCPEtcdBackup failed, It Should return error", + reason: hyperv1.BackupFailedReason, + wantErr: true, + errSubstr: "HCPEtcdBackup failed", + }, + { + name: "When HCPEtcdBackup is rejected, It Should return error", + reason: hyperv1.BackupRejectedReason, + wantErr: true, + errSubstr: "rejected", + }, + { + name: "When etcd is unhealthy, It Should return error", + reason: hyperv1.EtcdUnhealthyReason, + wantErr: true, + errSubstr: "etcd unhealthy", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + eb := &hyperv1.HCPEtcdBackup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-eb", Namespace: "clusters-test"}, + } + meta.SetStatusCondition(&eb.Status.Conditions, metav1.Condition{ + Type: string(hyperv1.BackupCompleted), + Status: metav1.ConditionFalse, + Reason: tt.reason, + Message: "test message", + }) + + client := testClient(scheme, eb) + o := &Orchestrator{ + log: logrus.New(), + client: client, + BackupName: "test-eb", + BackupNamespace: "clusters-test", + } + + err := o.VerifyInProgress(context.TODO()) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func TestWaitForCompletion(t *testing.T) { + scheme := testScheme() + + tests := []struct { + name string + reason string + status metav1.ConditionStatus + snapshotURL string + message string + wantErr bool + errSubstr string + wantURL string + }{ + { + name: "When HCPEtcdBackup succeeds with snapshotURL, It Should return the URL", + reason: hyperv1.BackupSucceededReason, + status: metav1.ConditionTrue, + snapshotURL: "s3://my-bucket/backups/test/etcd-backup/snapshot.db", + wantURL: "s3://my-bucket/backups/test/etcd-backup/snapshot.db", + }, + { + name: "When HCPEtcdBackup fails, It Should return error", + reason: hyperv1.BackupFailedReason, + status: metav1.ConditionFalse, + message: "etcd snapshot upload failed", + wantErr: true, + errSubstr: "HCPEtcdBackup failed", + }, + { + name: "When HCPEtcdBackup is rejected, It Should return error", + reason: hyperv1.BackupRejectedReason, + status: metav1.ConditionFalse, + message: "another backup in progress", + wantErr: true, + errSubstr: "rejected", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + eb := &hyperv1.HCPEtcdBackup{ + ObjectMeta: metav1.ObjectMeta{Name: "test-eb", Namespace: "clusters-test"}, + Status: hyperv1.HCPEtcdBackupStatus{SnapshotURL: tt.snapshotURL}, + } + meta.SetStatusCondition(&eb.Status.Conditions, metav1.Condition{ + Type: string(hyperv1.BackupCompleted), + Status: tt.status, + Reason: tt.reason, + Message: tt.message, + }) + + client := testClient(scheme, eb) + o := &Orchestrator{ + log: logrus.New(), + client: client, + BackupName: "test-eb", + BackupNamespace: "clusters-test", + } + + url, err := o.WaitForCompletion(context.TODO()) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.errSubstr)) + return + } + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(url).To(Equal(tt.wantURL)) + }) + } }