From d399ad1a587c11f7ef2dbb9b3a22f37dee4faae7 Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Mon, 9 Mar 2026 22:37:33 +0100 Subject: [PATCH 1/2] feat(api): register HCPEtcdBackup feature gate Enable the HCPEtcdBackup feature gate so the new CRD can be selectively activated per feature set. This is required before defining the CRD API types. Register the HCPEtcdBackup feature constant and variable in the hypershift-operator featuregate package, gated behind TechPreviewNoUpgrade. Update the four featuregate YAML manifests (Hypershift and SelfManagedHA, Default and TechPreviewNoUpgrade) and add unit tests covering all feature sets. Signed-off-by: Juan Manuel Parrilla Madrid Co-Authored-By: Claude Opus 4.6 --- .../featureGate-Hypershift-Default.yaml | 3 ++ ...eGate-Hypershift-TechPreviewNoUpgrade.yaml | 3 ++ .../featureGate-SelfManagedHA-Default.yaml | 3 ++ ...te-SelfManagedHA-TechPreviewNoUpgrade.yaml | 3 ++ hypershift-operator/featuregate/feature.go | 8 ++++ .../featuregate/feature_test.go | 47 +++++++++++++++++++ 6 files changed, 67 insertions(+) diff --git a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml index 46496a82ea6a..dafa030ea845 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-Default.yaml @@ -32,6 +32,9 @@ }, { "name": "GCPPlatform" + }, + { + "name": "HCPEtcdBackup" } ], "enabled": [ diff --git a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml index e6b9829665fd..3fd33ceac830 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml @@ -42,6 +42,9 @@ }, { "name": "GCPPlatform" + }, + { + "name": "HCPEtcdBackup" } ], "version": "" diff --git a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml index c01e9915886e..84bf5a8e1d73 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-Default.yaml @@ -32,6 +32,9 @@ }, { "name": "GCPPlatform" + }, + { + "name": "HCPEtcdBackup" } ], "enabled": [ diff --git a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-TechPreviewNoUpgrade.yaml b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-TechPreviewNoUpgrade.yaml index cea5c43cb51b..479143480aa2 100644 --- a/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-TechPreviewNoUpgrade.yaml +++ b/api/hypershift/v1beta1/featuregates/featureGate-SelfManagedHA-TechPreviewNoUpgrade.yaml @@ -42,6 +42,9 @@ }, { "name": "GCPPlatform" + }, + { + "name": "HCPEtcdBackup" } ], "version": "" diff --git a/hypershift-operator/featuregate/feature.go b/hypershift-operator/featuregate/feature.go index 556004873398..1a01c39484b2 100644 --- a/hypershift-operator/featuregate/feature.go +++ b/hypershift-operator/featuregate/feature.go @@ -28,6 +28,12 @@ const ( // alpha: v0.1.49 // beta: x.y.z GCPPlatform featuregate.Feature = "GCPPlatform" + + // HCPEtcdBackup enables the HCPEtcdBackup CRD for OADP-driven etcd backup orchestration. + // owner: @jparrill + // alpha: v0.1.49 + // beta: x.y.z + HCPEtcdBackup featuregate.Feature = "HCPEtcdBackup" ) // Initialize new features here @@ -37,6 +43,7 @@ var ( aroHCPManagedIdentitiesFeature = featuregates.NewFeature(AROHCPManagedIdentities, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade)) openStackFeature = featuregates.NewFeature(OpenStack, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade)) gcpHCPFeature = featuregates.NewFeature(GCPPlatform, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade)) + hcpEtcdBackupFeature = featuregates.NewFeature(HCPEtcdBackup, featuregates.WithEnableForFeatureSets(configv1.TechPreviewNoUpgrade)) ) func init() { @@ -44,6 +51,7 @@ func init() { allFeatures.AddFeature(aroHCPManagedIdentitiesFeature) allFeatures.AddFeature(openStackFeature) allFeatures.AddFeature(gcpHCPFeature) + allFeatures.AddFeature(hcpEtcdBackupFeature) // Default to configuring the Default featureset ConfigureFeatureSet(string(configv1.Default)) diff --git a/hypershift-operator/featuregate/feature_test.go b/hypershift-operator/featuregate/feature_test.go index af3f4b561f55..43b98482378c 100644 --- a/hypershift-operator/featuregate/feature_test.go +++ b/hypershift-operator/featuregate/feature_test.go @@ -10,6 +10,43 @@ import ( "github.com/stretchr/testify/assert" ) +func TestHCPEtcdBackupFeatureGate(t *testing.T) { + testcases := []struct { + name string + featureSet configv1.FeatureSet + expectedHCPEtcdBackup bool + }{ + { + name: "Default feature set should disable HCPEtcdBackup", + featureSet: configv1.Default, + expectedHCPEtcdBackup: false, + }, + { + name: "TechPreviewNoUpgrade feature set should enable HCPEtcdBackup", + featureSet: configv1.TechPreviewNoUpgrade, + expectedHCPEtcdBackup: true, + }, + { + name: "DevPreviewNoUpgrade feature set should disable HCPEtcdBackup", + featureSet: configv1.DevPreviewNoUpgrade, + expectedHCPEtcdBackup: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + featuregate.ConfigureFeatureSet(string(tc.featureSet)) + + actualHCPEtcdBackup := featuregate.Gate().Enabled(featuregate.HCPEtcdBackup) + assert.Equal(t, tc.expectedHCPEtcdBackup, actualHCPEtcdBackup, + "HCPEtcdBackup feature gate enabled state should match expected value for feature set %s", tc.featureSet) + + assert.Equal(t, tc.featureSet, featuregate.FeatureSet(), + "Feature set should be correctly configured") + }) + } +} + func TestGCPPlatformFeatureGate(t *testing.T) { testcases := []struct { name string @@ -63,6 +100,7 @@ func TestAllHypershiftOperatorFeatureGates(t *testing.T) { "AROHCPManagedIdentities": false, "OpenStack": false, "GCPPlatform": false, + "HCPEtcdBackup": false, }, }, { @@ -72,6 +110,7 @@ func TestAllHypershiftOperatorFeatureGates(t *testing.T) { "AROHCPManagedIdentities": true, "OpenStack": true, "GCPPlatform": true, + "HCPEtcdBackup": true, }, }, { @@ -81,6 +120,7 @@ func TestAllHypershiftOperatorFeatureGates(t *testing.T) { "AROHCPManagedIdentities": false, "OpenStack": false, "GCPPlatform": false, + "HCPEtcdBackup": false, }, }, } @@ -107,6 +147,12 @@ func TestAllHypershiftOperatorFeatureGates(t *testing.T) { assert.Equal(t, tc.expected["GCPPlatform"], actualGCPPlatform, "GCPPlatform should be %v for feature set %s", tc.expected["GCPPlatform"], tc.featureSet) + + // Test HCPEtcdBackup + actualHCPEtcdBackup := featuregate.Gate().Enabled(featuregate.HCPEtcdBackup) + assert.Equal(t, tc.expected["HCPEtcdBackup"], actualHCPEtcdBackup, + "HCPEtcdBackup should be %v for feature set %s", + tc.expected["HCPEtcdBackup"], tc.featureSet) }) } } @@ -116,4 +162,5 @@ func TestFeatureGateConstants(t *testing.T) { assert.Equal(t, "AROHCPManagedIdentities", string(featuregate.AROHCPManagedIdentities)) assert.Equal(t, "OpenStack", string(featuregate.OpenStack)) assert.Equal(t, "GCPPlatform", string(featuregate.GCPPlatform)) + assert.Equal(t, "HCPEtcdBackup", string(featuregate.HCPEtcdBackup)) } From 7080fce43e449e495dd9c5a7ddcdb002359001ff Mon Sep 17 00:00:00 2001 From: Juan Manuel Parrilla Madrid Date: Mon, 9 Mar 2026 22:46:32 +0100 Subject: [PATCH 2/2] feat(api): add HCPEtcdBackup CRD types and extend ManagedEtcdSpec with backup config Why: The HCPEtcdBackup feature requires new API types for the CRD and a backup configuration field in ManagedEtcdSpec to propagate encryption settings from HostedCluster to HostedControlPlane. How: - Define HCPEtcdBackup, HCPEtcdBackupSpec, HCPEtcdBackupStatus and supporting types (S3, AzureBlob, EncryptionMetadata) in etcdbackup_types.go with full validation markers. - Add HCPEtcdBackupConfig with platform-specific blocks (AWS/Azure) and CEL mutual exclusion validation. - Add Backup field to ManagedEtcdSpec behind HCPEtcdBackup feature gate. - Enforce spec immutability via CEL (one-shot backup request). - Enforce union discriminator via CEL (storageType must match backend). - Add regexp validation for kmsKeyARN (^arn:) and encryptionKeyURL (^https://) following existing patterns in aws.go. - Drop pointer for EncryptionMetadata in status (no nil vs zero semantic difference per OpenShift conventions). - Regenerate deepcopy and CRD manifests via make hypershift-api. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Juan Manuel Parrilla Madrid --- api/hypershift/v1beta1/etcdbackup_types.go | 366 + api/hypershift/v1beta1/hostedcluster_types.go | 7 + .../v1beta1/zz_generated.deepcopy.go | 257 + ..._generated.featuregated-crd-manifests.yaml | 37 + .../HCPEtcdBackup.yaml | 419 ++ .../HCPEtcdBackup.yaml | 6673 +++++++++++++++++ .../HCPEtcdBackup.yaml | 6536 ++++++++++++++++ .../hypershift/v1beta1/hcpetcdbackup.go | 241 + .../v1beta1/hcpetcdbackupazureblob.go | 74 + .../hypershift/v1beta1/hcpetcdbackupconfig.go | 60 + .../v1beta1/hcpetcdbackupconfigaws.go | 38 + .../v1beta1/hcpetcdbackupconfigazure.go | 38 + .../hcpetcdbackupencryptionmetadata.go | 47 + .../hcpetcdbackupencryptionmetadataaws.go | 38 + .../hcpetcdbackupencryptionmetadataazure.go | 38 + .../hypershift/v1beta1/hcpetcdbackups3.go | 74 + .../hypershift/v1beta1/hcpetcdbackupspec.go | 38 + .../hypershift/v1beta1/hcpetcdbackupstatus.go | 65 + .../v1beta1/hcpetcdbackupstorage.go | 60 + .../hypershift/v1beta1/managedetcdspec.go | 9 + .../hypershift/v1beta1/secretreference.go | 38 + client/applyconfiguration/utils.go | 26 + .../v1beta1/fake/fake_hcpetcdbackup.go | 52 + .../v1beta1/fake/fake_hypershift_client.go | 4 + .../hypershift/v1beta1/generated_expansion.go | 2 + .../typed/hypershift/v1beta1/hcpetcdbackup.go | 73 + .../hypershift/v1beta1/hypershift_client.go | 5 + client/informers/externalversions/generic.go | 2 + .../hypershift/v1beta1/hcpetcdbackup.go | 101 + .../hypershift/v1beta1/interface.go | 7 + .../hypershift/v1beta1/expansion_generated.go | 8 + .../hypershift/v1beta1/hcpetcdbackup.go | 69 + .../hcpetcdbackups-CustomNoUpgrade.crd.yaml | 422 ++ ...petcdbackups-TechPreviewNoUpgrade.crd.yaml | 422 ++ ...usters-Hypershift-CustomNoUpgrade.crd.yaml | 71 + ...s-Hypershift-TechPreviewNoUpgrade.crd.yaml | 71 + ...planes-Hypershift-CustomNoUpgrade.crd.yaml | 71 + ...s-Hypershift-TechPreviewNoUpgrade.crd.yaml | 71 + docs/content/reference/aggregated-docs.md | 987 ++- docs/content/reference/api.md | 987 ++- .../hypershift/v1beta1/etcdbackup_types.go | 366 + .../hypershift/v1beta1/hostedcluster_types.go | 7 + .../v1beta1/zz_generated.deepcopy.go | 257 + ..._generated.featuregated-crd-manifests.yaml | 37 + 44 files changed, 19033 insertions(+), 238 deletions(-) create mode 100644 api/hypershift/v1beta1/etcdbackup_types.go create mode 100644 api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hcpetcdbackups.hypershift.openshift.io/HCPEtcdBackup.yaml create mode 100644 api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml create mode 100644 api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackup.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupazureblob.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupconfig.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupconfigaws.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupconfigazure.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupencryptionmetadata.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupencryptionmetadataaws.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupencryptionmetadataazure.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackups3.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupspec.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupstatus.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/hcpetcdbackupstorage.go create mode 100644 client/applyconfiguration/hypershift/v1beta1/secretreference.go create mode 100644 client/clientset/clientset/typed/hypershift/v1beta1/fake/fake_hcpetcdbackup.go create mode 100644 client/clientset/clientset/typed/hypershift/v1beta1/hcpetcdbackup.go create mode 100644 client/informers/externalversions/hypershift/v1beta1/hcpetcdbackup.go create mode 100644 client/listers/hypershift/v1beta1/hcpetcdbackup.go create mode 100644 cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hcpetcdbackups-CustomNoUpgrade.crd.yaml create mode 100644 cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hcpetcdbackups-TechPreviewNoUpgrade.crd.yaml create mode 100644 vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/etcdbackup_types.go diff --git a/api/hypershift/v1beta1/etcdbackup_types.go b/api/hypershift/v1beta1/etcdbackup_types.go new file mode 100644 index 000000000000..915400537359 --- /dev/null +++ b/api/hypershift/v1beta1/etcdbackup_types.go @@ -0,0 +1,366 @@ +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" + BackupAlreadyInProgressReason string = "BackupAlreadyInProgress" + 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=512 + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getScheme() == 'https'",message="encryptionKeyURL must be a valid HTTPS URL" + // +kubebuilder:validation:XValidation:rule="url(self).getHostname().matches('[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net$')",message="encryptionKeyURL must point to an Azure Key Vault (*.vault.azure.net)" + // +kubebuilder:validation:XValidation:rule="url(self).getEscapedPath().matches('^/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL path must be /keys/ or /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="isURL(self)",message="snapshotURL must be a valid URL" + // +kubebuilder:validation:XValidation:rule="url(self).getScheme() == 'https' || url(self).getScheme() == 's3'",message="snapshotURL scheme must be 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=512 + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getScheme() == 'https'",message="encryptionKeyURL must be a valid HTTPS URL" + // +kubebuilder:validation:XValidation:rule="url(self).getHostname().matches('[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net$')",message="encryptionKeyURL must point to an Azure Key Vault (*.vault.azure.net)" + // +kubebuilder:validation:XValidation:rule="url(self).getEscapedPath().matches('^/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL path must be /keys/ or /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=512 + // +kubebuilder:validation:XValidation:rule="isURL(self) && url(self).getScheme() == 'https'",message="encryptionKeyURL must be a valid HTTPS URL" + // +kubebuilder:validation:XValidation:rule="url(self).getHostname().matches('[a-zA-Z0-9-]+\\\\.vault\\\\.azure\\\\.net$')",message="encryptionKeyURL must point to an Azure Key Vault (*.vault.azure.net)" + // +kubebuilder:validation:XValidation:rule="url(self).getEscapedPath().matches('^/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$')",message="encryptionKeyURL path must be /keys/ or /keys//" + EncryptionKeyURL string `json:"encryptionKeyURL,omitempty"` +} diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go index 92e1852fb2ca..a32b846952be 100644 --- a/api/hypershift/v1beta1/hostedcluster_types.go +++ b/api/hypershift/v1beta1/hostedcluster_types.go @@ -1885,6 +1885,13 @@ type ManagedEtcdSpec struct { // storage specifies how etcd data is persisted. // +required 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. diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go index c1d7cc029c95..404f1ad6cef5 100644 --- a/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -1822,6 +1822,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 @@ -2950,6 +3191,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. @@ -3945,6 +4187,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/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml index db8ddc7b22e1..55961c584029 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml @@ -125,6 +125,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,6 +174,7 @@ hostedclusters.hypershift.openshift.io: - ExternalOIDCWithUIDAndExtraClaimMappings - ExternalOIDCWithUpstreamParity - GCPPlatform + - HCPEtcdBackup - HyperShiftOnlyDynamicResourceAllocation - ImageStreamImportMode - KMSEncryptionProvider @@ -198,6 +234,7 @@ hostedcontrolplanes.hypershift.openshift.io: - ExternalOIDCWithUIDAndExtraClaimMappings - ExternalOIDCWithUpstreamParity - GCPPlatform + - HCPEtcdBackup - HyperShiftOnlyDynamicResourceAllocation - ImageStreamImportMode - KMSEncryptionProvider diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hcpetcdbackups.hypershift.openshift.io/HCPEtcdBackup.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hcpetcdbackups.hypershift.openshift.io/HCPEtcdBackup.yaml new file mode 100644 index 000000000000..4b04cd79d9be --- /dev/null +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hcpetcdbackups.hypershift.openshift.io/HCPEtcdBackup.yaml @@ -0,0 +1,419 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + feature-gate.release.openshift.io/HCPEtcdBackup: "true" + name: hcpetcdbackups.hypershift.openshift.io +spec: + group: hypershift.openshift.io + names: + kind: HCPEtcdBackup + listKind: HCPEtcdBackupList + plural: hcpetcdbackups + shortNames: + - hcpetcdbk + singular: hcpetcdbackup + scope: Namespaced + versions: + - additionalPrinterColumns: + - 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 + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + HCPEtcdBackup represents a request to back up etcd for a hosted control plane. + This resource is feature-gated behind the HCPEtcdBackup feature gate. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is the specification for the HCPEtcdBackup. + properties: + storage: + description: storage defines the cloud storage backend where the etcd + snapshot will be uploaded. + properties: + azureBlob: + description: |- + azureBlob specifies the Azure Blob storage configuration for the etcd backup. + Required when storageType is "AzureBlob", and forbidden otherwise. + properties: + container: + description: |- + 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 + maxLength: 63 + minLength: 3 + type: string + x-kubernetes-validations: + - message: container must consist of lowercase letters, numbers, + and hyphens, and must start and end with a letter or number + rule: self.matches('^[a-z0-9]([a-z0-9-]*[a-z0-9])?$') + - message: container must not contain consecutive hyphens + rule: '!self.contains(''--'')' + credentials: + description: |- + credentials references a Secret containing Azure credentials for uploading + to Blob Storage. The Secret must exist in the Hypershift Operator namespace. + properties: + name: + description: |- + 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. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must consist only of lowercase alphanumeric + characters, hyphens, and periods. Each period-separated + segment must start and end with an alphanumeric character. + rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$') + required: + - name + type: object + encryptionKeyURL: + description: |- + 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. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: encryptionKeyURL must be a valid HTTPS URL + rule: isURL(self) && url(self).getScheme() == 'https' + - message: encryptionKeyURL must point to an Azure Key Vault + (*.vault.azure.net) + rule: url(self).getHostname().matches('[a-zA-Z0-9-]+\\.vault\\.azure\\.net$') + - message: encryptionKeyURL path must be /keys/ + or /keys// + rule: url(self).getEscapedPath().matches('^/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$') + - message: encryptionKeyURL is immutable + rule: self == oldSelf + keyPrefix: + description: |- + 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 + maxLength: 1024 + minLength: 1 + type: string + x-kubernetes-validations: + - message: keyPrefix must consist of alphanumeric characters, + forward slashes, hyphens, underscores, and periods + rule: self.matches('^[a-zA-Z0-9/_.-]+$') + storageAccount: + description: |- + 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 + maxLength: 24 + minLength: 3 + type: string + x-kubernetes-validations: + - message: storageAccount must consist of lowercase letters + and numbers only + rule: self.matches('^[a-z0-9]+$') + required: + - container + - credentials + - keyPrefix + - storageAccount + type: object + x-kubernetes-validations: + - message: encryptionKeyURL cannot be removed once set + rule: '!has(oldSelf.encryptionKeyURL) || has(self.encryptionKeyURL)' + s3: + description: |- + s3 specifies the S3 storage configuration for the etcd backup. + Required when storageType is "S3", and forbidden otherwise. + properties: + bucket: + description: |- + 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 + maxLength: 63 + minLength: 3 + type: string + x-kubernetes-validations: + - message: bucket must consist of lowercase letters, numbers, + hyphens, and periods, and must start and end with a letter + or number + rule: self.matches('^[a-z0-9][a-z0-9.-]*[a-z0-9]$') + - message: bucket must not contain consecutive periods + rule: '!self.contains(''..'')' + credentials: + description: |- + 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. + properties: + name: + description: |- + 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. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must consist only of lowercase alphanumeric + characters, hyphens, and periods. Each period-separated + segment must start and end with an alphanumeric character. + rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$') + required: + - name + type: object + keyPrefix: + description: |- + 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 + maxLength: 1024 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'keyPrefix must consist of safe S3 key characters: + alphanumeric characters, forward slashes, hyphens, underscores, + periods, exclamation marks, asterisks, single quotes, + and parentheses' + rule: self.matches('^[a-zA-Z0-9!_.*\'()/-]+$') + kmsKeyARN: + description: |- + 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. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: kmsKeyARN must be a valid AWS KMS key ARN (arn::kms:::key/) + rule: self.matches('^arn:(aws|aws-cn|aws-us-gov):kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$') + - message: kmsKeyARN is immutable + rule: self == oldSelf + region: + description: |- + 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. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: region must consist of lowercase letters, digits, + and hyphens, must start with a letter and end with an + alphanumeric character + rule: self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') + - message: region must not contain consecutive hyphens + rule: '!self.contains(''--'')' + required: + - bucket + - credentials + - keyPrefix + - region + type: object + x-kubernetes-validations: + - message: kmsKeyARN cannot be removed once set + rule: '!has(oldSelf.kmsKeyARN) || has(self.kmsKeyARN)' + storageType: + description: |- + 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. + enum: + - S3 + - AzureBlob + type: string + required: + - storageType + type: object + x-kubernetes-validations: + - message: s3 configuration is required when storageType is S3, and + forbidden otherwise + rule: 'self.storageType == ''S3'' ? has(self.s3) : !has(self.s3)' + - message: azureBlob configuration is required when storageType is + AzureBlob, and forbidden otherwise + rule: 'self.storageType == ''AzureBlob'' ? has(self.azureBlob) : + !has(self.azureBlob)' + required: + - storage + type: object + x-kubernetes-validations: + - message: HCPEtcdBackupSpec is immutable + rule: self == oldSelf + status: + description: status is the status of the HCPEtcdBackup. + minProperties: 1 + properties: + conditions: + description: |- + 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). + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 10 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + encryptionMetadata: + description: |- + encryptionMetadata contains metadata about the encryption of the backup. + When present, at least one platform-specific encryption block must be set. + maxProperties: 1 + minProperties: 1 + properties: + aws: + description: aws contains AWS-specific encryption metadata for + the backup. + properties: + kmsKeyARN: + description: |- + 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. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: kmsKeyARN must be a valid AWS KMS key ARN (arn::kms:::key/) + rule: self.matches('^arn:(aws|aws-cn|aws-us-gov):kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$') + required: + - kmsKeyARN + type: object + azure: + description: azure contains Azure-specific encryption metadata + for the backup. + properties: + encryptionKeyURL: + description: |- + 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/[/]". + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: encryptionKeyURL must be a valid HTTPS URL + rule: isURL(self) && url(self).getScheme() == 'https' + - message: encryptionKeyURL must point to an Azure Key Vault + (*.vault.azure.net) + rule: url(self).getHostname().matches('[a-zA-Z0-9-]+\\.vault\\.azure\\.net$') + - message: encryptionKeyURL path must be /keys/ + or /keys// + rule: url(self).getEscapedPath().matches('^/keys/[a-zA-Z0-9-]+(/[a-zA-Z0-9]+)?$') + required: + - encryptionKeyURL + type: object + type: object + snapshotURL: + description: |- + snapshotURL is the URL of the completed backup snapshot in cloud storage. + Must be a valid URL with scheme https or s3. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: snapshotURL must be a valid URL + rule: isURL(self) + - message: snapshotURL scheme must be https or s3 + rule: url(self).getScheme() == 'https' || url(self).getScheme() + == 's3' + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} 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 new file mode 100644 index 000000000000..281868c713f7 --- /dev/null +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml @@ -0,0 +1,6673 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + feature-gate.release.openshift.io/HCPEtcdBackup: "true" + name: hostedclusters.hypershift.openshift.io +spec: + group: hypershift.openshift.io + names: + kind: HostedCluster + listKind: HostedClusterList + plural: hostedclusters + shortNames: + - hc + - hcs + singular: hostedcluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Version + jsonPath: .status.version.history[?(@.state=="Completed")].version + name: Version + type: string + - description: KubeConfig Secret + jsonPath: .status.kubeconfig.name + name: KubeConfig + type: string + - description: Progress + jsonPath: .status.version.history[?(@.state!="")].state + name: Progress + type: string + - description: Available + jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available + type: string + - description: Progressing + jsonPath: .status.conditions[?(@.type=="Progressing")].status + name: Progressing + type: string + - description: Message + jsonPath: .status.conditions[?(@.type=="Available")].message + name: Message + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + HostedCluster is the primary representation of a HyperShift cluster and encapsulates + the control plane and common data plane configuration. Creating a HostedCluster + results in a fully functional OpenShift control plane with no attached nodes. + To support workloads (e.g. pods), a HostedCluster may have one or more associated + NodePool resources. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is the desired behavior of the HostedCluster. + properties: + additionalTrustBundle: + description: |- + additionalTrustBundle is a local reference to a ConfigMap that must have a "ca-bundle.crt" key + whose content must be a PEM-encoded X.509 certificate bundle that will be added to the hosted controlplane and nodes + If the reference is set but none of the above requirements are met, the HostedCluster will enter a degraded state. + This will be part of every payload generated by the controllers for any NodePool of the HostedCluster. + Changing this value will trigger a rollout for all existing NodePools in the cluster. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + auditWebhook: + description: |- + auditWebhook contains metadata for configuring an audit webhook endpoint + for a cluster to process cluster audit events. It references a secret that + contains the webhook information for the audit webhook endpoint. It is a + secret because if the endpoint has mTLS the kubeconfig will contain client + keys. The kubeconfig needs to be stored in the secret with a secret key + name that corresponds to the constant AuditWebhookKubeconfigKey. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + autoscaling: + description: |- + autoscaling specifies auto-scaling behavior that applies to all NodePools + associated with this HostedCluster. + properties: + balancingIgnoredLabels: + description: |- + balancingIgnoredLabels sets "--balancing-ignore-label