diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go index b80f13462bab..d99f765f090f 100644 --- a/api/hypershift/v1beta1/hostedcluster_types.go +++ b/api/hypershift/v1beta1/hostedcluster_types.go @@ -1953,7 +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 == 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"` } @@ -2180,6 +2181,16 @@ type HostedClusterStatus struct { // 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. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml index 2728d21fb954..b5222bb3375b 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml @@ -2529,6 +2529,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml index 4144f1221233..70641ae031d8 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml @@ -2656,6 +2656,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml index fa2ff2076c00..cc3c053f800a 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml @@ -2520,6 +2520,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml index e420c333520c..de1dc8aa58af 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml @@ -2520,6 +2520,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml index 1e8b6ee91820..6630d27aeb21 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml @@ -2853,6 +2853,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml index 7b69662e9c4d..d4c1bdd7fcf2 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml @@ -2993,6 +2993,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml index eb1675967890..7822b30358e7 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml @@ -2974,6 +2974,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml index 5f7edb99161e..6c6fdc61639e 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml @@ -2520,6 +2520,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml index 639559bd579a..9f9425548989 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml @@ -2585,6 +2585,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. @@ -6688,6 +6691,18 @@ spec: type: string type: object x-kubernetes-map-type: atomic + lastSuccessfulEtcdBackupURL: + description: |- + 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. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: lastSuccessfulEtcdBackupURL must be a valid URL with scheme + https or s3 + rule: self.matches('^(https|s3)://.*') oauthCallbackURLTemplate: description: |- oauthCallbackURLTemplate contains a template for the URL to use as a callback diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml index 85517eabe63b..45c638c0545a 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml @@ -2542,6 +2542,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml index 359a05a454cb..1dd0ce8473d5 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml @@ -2538,6 +2538,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryptionProvider.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryptionProvider.yaml index af0021330081..0fbf3783689c 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryptionProvider.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryptionProvider.yaml @@ -2596,6 +2596,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml index 45f7d4281e33..a9550f13ec18 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml @@ -2520,6 +2520,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml index 7bbd3b9403d2..9a49d67f9eda 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml @@ -2446,6 +2446,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml index 090458227c3e..04be6dd42570 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml @@ -2575,6 +2575,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml index db479139d40b..4181eaa306c3 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml @@ -2437,6 +2437,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml index 011843889c3f..e6615973f4b7 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml @@ -2437,6 +2437,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml index 85ecaf3a151b..5fbcb4772f82 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml @@ -2770,6 +2770,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml index 467ef11d489a..84fef65195b8 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml @@ -2910,6 +2910,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml index 61e4fe637380..5a85ccca1e67 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml @@ -2891,6 +2891,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml index 57aebd76bda7..c859a25b2c1d 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml @@ -2437,6 +2437,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml index e0198f238e5c..1d9fa278fff6 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml @@ -2502,6 +2502,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml index cbc9011e3bef..d1aea3cc45ae 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml @@ -2459,6 +2459,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml index 2f88c0df3bf8..4594af147b5a 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml @@ -2455,6 +2455,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryptionProvider.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryptionProvider.yaml index 084976b65e78..d252df9539c9 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryptionProvider.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryptionProvider.yaml @@ -2513,6 +2513,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml index b7a413503c33..c5516bc62f51 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml @@ -2437,6 +2437,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go b/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go index 42115bfa1e97..4ca327284e20 100644 --- a/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go +++ b/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go @@ -26,19 +26,20 @@ import ( // HostedClusterStatusApplyConfiguration represents a declarative configuration of the HostedClusterStatus type for use // with apply. type HostedClusterStatusApplyConfiguration struct { - Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` - ControlPlaneVersion *ControlPlaneVersionStatusApplyConfiguration `json:"controlPlaneVersion,omitempty"` - Version *ClusterVersionStatusApplyConfiguration `json:"version,omitempty"` - KubeConfig *corev1.LocalObjectReference `json:"kubeconfig,omitempty"` - CustomKubeconfig *corev1.LocalObjectReference `json:"customKubeconfig,omitempty"` - KubeadminPassword *corev1.LocalObjectReference `json:"kubeadminPassword,omitempty"` - IgnitionEndpoint *string `json:"ignitionEndpoint,omitempty"` - ControlPlaneEndpoint *APIEndpointApplyConfiguration `json:"controlPlaneEndpoint,omitempty"` - OAuthCallbackURLTemplate *string `json:"oauthCallbackURLTemplate,omitempty"` - PayloadArch *hypershiftv1beta1.PayloadArchType `json:"payloadArch,omitempty"` - Platform *PlatformStatusApplyConfiguration `json:"platform,omitempty"` - AutoNode *AutoNodeStatusApplyConfiguration `json:"autoNode,omitempty"` - Configuration *ConfigurationStatusApplyConfiguration `json:"configuration,omitempty"` + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + ControlPlaneVersion *ControlPlaneVersionStatusApplyConfiguration `json:"controlPlaneVersion,omitempty"` + Version *ClusterVersionStatusApplyConfiguration `json:"version,omitempty"` + KubeConfig *corev1.LocalObjectReference `json:"kubeconfig,omitempty"` + CustomKubeconfig *corev1.LocalObjectReference `json:"customKubeconfig,omitempty"` + KubeadminPassword *corev1.LocalObjectReference `json:"kubeadminPassword,omitempty"` + IgnitionEndpoint *string `json:"ignitionEndpoint,omitempty"` + ControlPlaneEndpoint *APIEndpointApplyConfiguration `json:"controlPlaneEndpoint,omitempty"` + OAuthCallbackURLTemplate *string `json:"oauthCallbackURLTemplate,omitempty"` + PayloadArch *hypershiftv1beta1.PayloadArchType `json:"payloadArch,omitempty"` + Platform *PlatformStatusApplyConfiguration `json:"platform,omitempty"` + AutoNode *AutoNodeStatusApplyConfiguration `json:"autoNode,omitempty"` + Configuration *ConfigurationStatusApplyConfiguration `json:"configuration,omitempty"` + LastSuccessfulEtcdBackupURL *string `json:"lastSuccessfulEtcdBackupURL,omitempty"` } // HostedClusterStatusApplyConfiguration constructs a declarative configuration of the HostedClusterStatus type for use with @@ -155,3 +156,11 @@ func (b *HostedClusterStatusApplyConfiguration) WithConfiguration(value *Configu b.Configuration = value return b } + +// WithLastSuccessfulEtcdBackupURL sets the LastSuccessfulEtcdBackupURL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LastSuccessfulEtcdBackupURL field is set to the value of the last call. +func (b *HostedClusterStatusApplyConfiguration) WithLastSuccessfulEtcdBackupURL(value string) *HostedClusterStatusApplyConfiguration { + b.LastSuccessfulEtcdBackupURL = &value + return b +} diff --git a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/featuregated.hostedclusters.etcdbackup.testsuite.yaml b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/featuregated.hostedclusters.etcdbackup.testsuite.yaml new file mode 100644 index 000000000000..063199d588a1 --- /dev/null +++ b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/featuregated.hostedclusters.etcdbackup.testsuite.yaml @@ -0,0 +1,266 @@ +apiVersion: apiextensions.k8s.io/v1 +name: "HostedCluster etcd backup status validation" +crdName: hostedclusters.hypershift.openshift.io +featureGates: + - HCPEtcdBackup +version: v1beta1 +tests: + onUpdate: + - name: When lastSuccessfulEtcdBackupURL has invalid scheme it should fail + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + updated: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + status: + lastSuccessfulEtcdBackupURL: "http://bucket.example.com/snapshot.db" + expectedStatusError: "lastSuccessfulEtcdBackupURL must be a valid URL with scheme https or s3" + + - name: When lastSuccessfulEtcdBackupURL has https scheme it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + updated: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + status: + lastSuccessfulEtcdBackupURL: "https://bucket.s3.amazonaws.com/snapshot.db" + + - name: When lastSuccessfulEtcdBackupURL has s3 scheme it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + updated: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + status: + lastSuccessfulEtcdBackupURL: "s3://bucket/snapshot.db" diff --git a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.etcd.testsuite.yaml b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.etcd.testsuite.yaml index 4b51a7b0934d..ba471d7f5e15 100644 --- a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.etcd.testsuite.yaml +++ b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.etcd.testsuite.yaml @@ -96,6 +96,142 @@ tests: type: Route route: {} + - name: When restoreSnapshotURL has invalid scheme it should fail + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + restoreSnapshotURL: + - "http://backup.example.com/snapshot.db" + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + expectedError: "restoreSnapshotURL must be a valid URL with scheme https or s3" + + - name: When restoreSnapshotURL has https scheme it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + restoreSnapshotURL: + - "https://backup.example.com/snapshot.db" + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + + - name: When restoreSnapshotURL has s3 scheme it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + spec: + dns: + baseDomain: example.com + etcd: + managementType: Managed + managed: + storage: + type: PersistentVolume + persistentVolume: + size: 8Gi + restoreSnapshotURL: + - "s3://bucket/snapshot.db" + platform: + type: AWS + pullSecret: + name: secret + release: + image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64 + secretEncryption: + aescbc: + activeKey: + name: key + type: aescbc + services: + - service: APIServer + servicePublishingStrategy: + type: Route + route: {} + - service: OAuthServer + servicePublishingStrategy: + type: Route + route: {} + - service: Konnectivity + servicePublishingStrategy: + type: Route + route: {} + - service: Ignition + servicePublishingStrategy: + type: Route + route: {} + onUpdate: - name: When restoreSnapshotURL is changed it should fail initial: | diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml index ec0e1156a565..a3663b2d3ca9 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml @@ -3438,6 +3438,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. @@ -8718,6 +8721,18 @@ spec: type: string type: object x-kubernetes-map-type: atomic + lastSuccessfulEtcdBackupURL: + description: |- + 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. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: lastSuccessfulEtcdBackupURL must be a valid URL with scheme + https or s3 + rule: self.matches('^(https|s3)://.*') oauthCallbackURLTemplate: description: |- oauthCallbackURLTemplate contains a template for the URL to use as a callback diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml index 1cbee8c3935b..8e955327ae9f 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml @@ -3022,6 +3022,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml index ea538f3de603..72f606f53dc0 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml @@ -3349,6 +3349,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. @@ -8618,6 +8621,18 @@ spec: type: string type: object x-kubernetes-map-type: atomic + lastSuccessfulEtcdBackupURL: + description: |- + 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. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: lastSuccessfulEtcdBackupURL must be a valid URL with scheme + https or s3 + rule: self.matches('^(https|s3)://.*') oauthCallbackURLTemplate: description: |- oauthCallbackURLTemplate contains a template for the URL to use as a callback diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml index 882f2ddbd142..67a307c95f3a 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml @@ -3357,6 +3357,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml index ea7aaac0edd6..7207fcca0821 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml @@ -2939,6 +2939,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml index af5005432821..42063effc660 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml @@ -3268,6 +3268,9 @@ spec: rule: self.size() <= 1 - message: restoreSnapshotURL is immutable rule: self == oldSelf + - message: restoreSnapshotURL must be a valid URL with + scheme https or s3 + rule: self.size() == 0 || self[0].matches('^(https|s3)://.*') type: description: |- type is the kind of persistent storage implementation to use for etcd. diff --git a/docs/content/reference/aggregated-docs.md b/docs/content/reference/aggregated-docs.md index af071967d6a2..866b7e7edb9d 100644 --- a/docs/content/reference/aggregated-docs.md +++ b/docs/content/reference/aggregated-docs.md @@ -39888,6 +39888,20 @@ ConfigurationStatus

configuration contains the cluster configuration status of the HostedCluster

+ + +lastSuccessfulEtcdBackupURL
+ +string + + + +(Optional) +

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.

+ + ###HostedControlPlaneSpec { #hypershift.openshift.io/v1beta1.HostedControlPlaneSpec } diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 0b4345101814..1d6c5b273a43 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -9454,6 +9454,20 @@ ConfigurationStatus

configuration contains the cluster configuration status of the HostedCluster

+ + +lastSuccessfulEtcdBackupURL
+ +string + + + +(Optional) +

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.

+ + ###HostedControlPlaneSpec { #hypershift.openshift.io/v1beta1.HostedControlPlaneSpec } diff --git a/hypershift-operator/controllers/etcdbackup/reconciler.go b/hypershift-operator/controllers/etcdbackup/reconciler.go index 09299d5f5155..89055e4886ff 100644 --- a/hypershift-operator/controllers/etcdbackup/reconciler.go +++ b/hypershift-operator/controllers/etcdbackup/reconciler.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/util/retry" "k8s.io/client-go/util/workqueue" "k8s.io/utils/ptr" @@ -317,6 +318,22 @@ func (r *HCPEtcdBackupReconciler) updateHCPBackupCondition(ctx context.Context, return r.Status().Update(ctx, hcp) } +// updateHostedClusterBackupURL persists the snapshot URL in the HostedCluster +// status so it survives HCPEtcdBackup CR retention/deletion. +// Uses RetryOnConflict because the HC is updated by multiple controllers, +// and a requeue-based retry risks losing the URL if the Pod is cleaned up +// (TTLSecondsAfterFinished) before the next reconcile extracts it. +func (r *HCPEtcdBackupReconciler) updateHostedClusterBackupURL(ctx context.Context, hcp *hyperv1.HostedControlPlane, snapshotURL string) error { + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { + hc, err := hyperutil.HostedClusterFromAnnotation(ctx, r.Client, hcp) + if err != nil { + return err + } + hc.Status.LastSuccessfulEtcdBackupURL = snapshotURL + return r.Status().Update(ctx, hc) + }) +} + // getHostedControlPlane finds the HostedControlPlane in the given namespace. // Returns nil if none found. func (r *HCPEtcdBackupReconciler) getHostedControlPlane(ctx context.Context, namespace string) (*hyperv1.HostedControlPlane, error) { @@ -400,9 +417,27 @@ func (r *HCPEtcdBackupReconciler) handleJobStatus(ctx context.Context, backup *h // Extract snapshotURL from the upload container's termination message. // The etcd-upload command writes the URL to /dev/termination-log. - if url, err := r.getSnapshotURLFromPod(ctx, job); err != nil { + url, err := r.getSnapshotURLFromPod(ctx, job) + if err != nil { logger.Error(err, "failed to read snapshot URL from pod termination message") - } else if url != "" { + url = "" // don't use url on error + } + + // Cleanup temporary RBAC and NetworkPolicy as soon as the Job completes. + // This must happen before any status updates that could fail and cause + // requeue, to avoid leaving security resources exposed indefinitely. + if err := r.cleanupResources(ctx, backup); err != nil { + logger.Error(err, "failed to cleanup resources after successful backup") + } + + // Persist the snapshot URL in the HostedCluster status BEFORE marking + // the backup as terminal so the controller retries on requeue. + // This is idempotent: if it succeeds but the backup status update below + // fails, the next reconcile re-extracts the URL and writes the same value. + if url != "" { + if err := r.updateHostedClusterBackupURL(ctx, hcp, url); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update HostedCluster LastSuccessfulEtcdBackupURL: %w", err) + } backup.Status.SnapshotURL = url } @@ -430,15 +465,17 @@ func (r *HCPEtcdBackupReconciler) handleJobStatus(ctx context.Context, backup *h logger.Error(err, "failed to update HCP backup condition") } - if err := r.cleanupResources(ctx, backup); err != nil { - logger.Error(err, "failed to cleanup resources after successful backup") - } return ctrl.Result{}, nil } if cond.Type == batchv1.JobFailed && cond.Status == corev1.ConditionTrue { logger.Info("backup Job failed", "job", job.Name, "reason", cond.Message) + // Cleanup temporary resources immediately on Job termination. + if err := r.cleanupResources(ctx, backup); err != nil { + logger.Error(err, "failed to cleanup resources after failed backup") + } + r.setCondition(backup, metav1.Condition{ Type: string(hyperv1.BackupCompleted), Status: metav1.ConditionFalse, @@ -460,9 +497,6 @@ func (r *HCPEtcdBackupReconciler) handleJobStatus(ctx context.Context, backup *h logger.Error(err, "failed to update HCP backup condition") } - if err := r.cleanupResources(ctx, backup); err != nil { - logger.Error(err, "failed to cleanup resources after failed backup") - } return ctrl.Result{}, nil } } diff --git a/hypershift-operator/controllers/etcdbackup/reconciler_test.go b/hypershift-operator/controllers/etcdbackup/reconciler_test.go index ce4175145804..fa117e86d8a7 100644 --- a/hypershift-operator/controllers/etcdbackup/reconciler_test.go +++ b/hypershift-operator/controllers/etcdbackup/reconciler_test.go @@ -12,6 +12,7 @@ import ( hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1" "github.com/openshift/hypershift/hypershift-operator/featuregate" "github.com/openshift/hypershift/support/releaseinfo" + hyperutil "github.com/openshift/hypershift/support/util" configv1 "github.com/openshift/api/config/v1" imageapi "github.com/openshift/api/image/v1" @@ -21,15 +22,18 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/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/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) func TestMain(m *testing.M) { @@ -42,6 +46,8 @@ const ( testHONamespace = "hypershift" testBackupName = "backup-1" testHCPName = "test-hcp" + testHCNamespace = "clusters" + testHCName = "test" testReleaseImage = "quay.io/openshift-release-dev/ocp-release:4.16.0-x86_64" ) @@ -63,7 +69,7 @@ func newReconciler(objs ...client.Object) *HCPEtcdBackupReconciler { fakeClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(clientObjs...). - WithStatusSubresource(&hyperv1.HCPEtcdBackup{}, &hyperv1.HostedControlPlane{}). + WithStatusSubresource(&hyperv1.HCPEtcdBackup{}, &hyperv1.HostedControlPlane{}, &hyperv1.HostedCluster{}). Build() return &HCPEtcdBackupReconciler{ @@ -97,11 +103,23 @@ func newHCPEtcdBackup() *hyperv1.HCPEtcdBackup { } } +func newHostedCluster() *hyperv1.HostedCluster { + return &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: testHCName, + Namespace: testHCNamespace, + }, + } +} + func newHostedControlPlane() *hyperv1.HostedControlPlane { return &hyperv1.HostedControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: testHCPName, Namespace: testHCPNamespace, + Annotations: map[string]string{ + hyperutil.HostedClusterAnnotation: testHCNamespace + "/" + testHCName, + }, }, Spec: hyperv1.HostedControlPlaneSpec{ ReleaseImage: testReleaseImage, @@ -751,6 +769,7 @@ func TestHandleJobStatus(t *testing.T) { g := NewGomegaWithT(t) backup := newHCPEtcdBackup() hcp := newHostedControlPlane() + hc := newHostedCluster() job := newTestJob(batchv1.JobStatus{ Conditions: []batchv1.JobCondition{ { @@ -786,7 +805,7 @@ func TestHandleJobStatus(t *testing.T) { }, }, } - r := newReconciler(backup, job, hcp, pod) + r := newReconciler(backup, job, hcp, hc, pod) result, err := r.handleJobStatus(context.Background(), backup, job, hcp) g.Expect(err).ToNot(HaveOccurred()) @@ -805,6 +824,11 @@ func TestHandleJobStatus(t *testing.T) { g.Expect(hcpCond).ToNot(BeNil()) g.Expect(hcpCond.Status).To(Equal(metav1.ConditionTrue)) g.Expect(hcpCond.Reason).To(Equal(hyperv1.BackupSucceededReason)) + + // Verify HostedCluster status has the snapshot URL persisted + updatedHC := &hyperv1.HostedCluster{} + g.Expect(r.Get(context.Background(), types.NamespacedName{Name: testHCName, Namespace: testHCNamespace}, updatedHC)).To(Succeed()) + g.Expect(updatedHC.Status.LastSuccessfulEtcdBackupURL).To(Equal("s3://my-bucket/backups/test/snapshot.db")) }) t.Run("When Job fails it should set BackupFailed", func(t *testing.T) { @@ -1396,3 +1420,81 @@ func TestReconcileHappyPath(t *testing.T) { g.Expect(hcpCond.Reason).To(Equal(hyperv1.BackupInProgressReason)) }) } + +func TestUpdateHostedClusterBackupURL(t *testing.T) { + t.Run("When HCP has HostedClusterAnnotation it should update HC status with snapshot URL", func(t *testing.T) { + g := NewGomegaWithT(t) + hcp := newHostedControlPlane() + hc := newHostedCluster() + r := newReconciler(hcp, hc) + + err := r.updateHostedClusterBackupURL(context.Background(), hcp, "s3://bucket/snapshot.db") + g.Expect(err).ToNot(HaveOccurred()) + + updatedHC := &hyperv1.HostedCluster{} + g.Expect(r.Get(context.Background(), types.NamespacedName{Name: testHCName, Namespace: testHCNamespace}, updatedHC)).To(Succeed()) + g.Expect(updatedHC.Status.LastSuccessfulEtcdBackupURL).To(Equal("s3://bucket/snapshot.db")) + }) + + t.Run("When HCP is missing HostedClusterAnnotation it should return an error", func(t *testing.T) { + g := NewGomegaWithT(t) + hcp := newHostedControlPlane() + hcp.Annotations = nil + r := newReconciler(hcp) + + err := r.updateHostedClusterBackupURL(context.Background(), hcp, "s3://bucket/snapshot.db") + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("missing")) + }) + + t.Run("When HostedCluster does not exist it should return an error", func(t *testing.T) { + g := NewGomegaWithT(t) + hcp := newHostedControlPlane() + r := newReconciler(hcp) // no HC in the fake client + + err := r.updateHostedClusterBackupURL(context.Background(), hcp, "s3://bucket/snapshot.db") + g.Expect(err).To(HaveOccurred()) + }) + + t.Run("When HC status update conflicts it should retry and succeed", func(t *testing.T) { + g := NewGomegaWithT(t) + hcp := newHostedControlPlane() + hc := newHostedCluster() + + callCount := 0 + s := newScheme() + fakeClient := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(hcp, hc). + WithStatusSubresource(&hyperv1.HCPEtcdBackup{}, &hyperv1.HostedControlPlane{}, &hyperv1.HostedCluster{}). + WithInterceptorFuncs(interceptor.Funcs{ + SubResourceUpdate: func(ctx context.Context, cl client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error { + if _, ok := obj.(*hyperv1.HostedCluster); ok { + callCount++ + if callCount == 1 { + return apierrors.NewConflict( + schema.GroupResource{Group: "hypershift.openshift.io", Resource: "hostedclusters"}, + obj.GetName(), + fmt.Errorf("the object has been modified"), + ) + } + } + return cl.Status().Update(ctx, obj, opts...) + }, + }). + Build() + + r := &HCPEtcdBackupReconciler{ + Client: fakeClient, + OperatorNamespace: testHONamespace, + } + + err := r.updateHostedClusterBackupURL(context.Background(), hcp, "s3://bucket/snapshot.db") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(callCount).To(Equal(2)) + + updatedHC := &hyperv1.HostedCluster{} + g.Expect(r.Get(context.Background(), types.NamespacedName{Name: testHCName, Namespace: testHCNamespace}, updatedHC)).To(Succeed()) + g.Expect(updatedHC.Status.LastSuccessfulEtcdBackupURL).To(Equal("s3://bucket/snapshot.db")) + }) +} 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 b80f13462bab..d99f765f090f 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 @@ -1953,7 +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 == 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"` } @@ -2180,6 +2181,16 @@ type HostedClusterStatus struct { // 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.