Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,24 @@ linters:
path: hypershift/v1beta1/hosted_controlplane.go
text: 'statusoptional: status field "Ready" must be marked as optional, not required'

# requiredfields/optionalfields false positives on well-known k8s types (https://github.com/kubernetes-sigs/kube-api-linter/issues/249)
- linters:
- kubeapilinter
path: hypershift/v1beta1/hostedcluster_types.go
text: 'requiredfields: field EncryptionMigrationHistory.StartedTime has a valid zero value'
- linters:
- kubeapilinter
path: hypershift/v1beta1/hostedcluster_types.go
text: 'requiredfields: field EncryptionMigrationHistory.StartedTime should have the omitempty tag'
- linters:
- kubeapilinter
path: hypershift/v1beta1/hostedcluster_types.go
text: 'optionalfields: field EncryptionMigrationHistory.CompletionTime has a valid zero value'
- linters:
- kubeapilinter
path: hypershift/v1beta1/hostedcluster_types.go
text: 'optionalfields: field EncryptionMigrationHistory.CompletionTime should have the omitempty tag'

# uniquemarkers (3 issues)
- linters:
- kubeapilinter
Expand Down
8 changes: 6 additions & 2 deletions api/hypershift/v1beta1/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,9 @@ type AWSKMSSpec struct {
ActiveKey AWSKMSKeyEntry `json:"activeKey"`
// backupKey defines the old key during the rotation process so previously created
// secrets can continue to be decrypted until they are all re-encrypted with the active key.
//
// Deprecated: This field will be ignored when status.secretEncryption.activeKey is set.
// The system automatically manages the previous key via the status field.
// +optional
BackupKey *AWSKMSKeyEntry `json:"backupKey,omitempty"`
// auth defines metadata about the management of credentials used to interact with AWS KMS
Expand Down Expand Up @@ -1077,9 +1080,10 @@ type AWSKMSAuthSpec struct {
type AWSKMSKeyEntry struct {
// arn is the Amazon Resource Name for the encryption key
// +required
// +kubebuilder:validation:Pattern=`^arn:`
// +kubebuilder:validation:XValidation:rule="self.startsWith('arn:')",message="arn must start with 'arn:'"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=2048
ARN string `json:"arn"`
ARN string `json:"arn,omitempty"`
}

// AWSPlatformStatus contains status specific to the AWS platform
Expand Down
12 changes: 9 additions & 3 deletions api/hypershift/v1beta1/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,9 @@ type AzureKMSSpec struct {
ActiveKey AzureKMSKey `json:"activeKey"`
// backupKey defines the old key during the rotation process so previously created
// secrets can continue to be decrypted until they are all re-encrypted with the active key.
//
// Deprecated: This field will be ignored when status.secretEncryption.activeKey is set.
// The system automatically manages the previous key via the status field.
// +optional
BackupKey *AzureKMSKey `json:"backupKey,omitempty"`

Expand Down Expand Up @@ -887,19 +890,22 @@ type AzureKMSKey struct {
// keyVaultName is the name of the keyvault. Must match criteria specified at https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name
// Your Microsoft Entra application used to create the cluster must be authorized to access this keyvault, e.g using the AzureCLI:
// `az keyvault set-policy -n $KEYVAULT_NAME --key-permissions decrypt encrypt --spn <YOUR APPLICATION CLIENT ID>`
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
KeyVaultName string `json:"keyVaultName"`
KeyVaultName string `json:"keyVaultName,omitempty"`

// keyName is the name of the keyvault key used for encrypt/decrypt
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
KeyName string `json:"keyName"`
KeyName string `json:"keyName,omitempty"`

// keyVersion contains the version of the key to use
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
KeyVersion string `json:"keyVersion"`
KeyVersion string `json:"keyVersion,omitempty"`
}

// AzureAuthenticationType is a discriminated union type that contains the Azure authentication configuration for an
Expand Down
5 changes: 5 additions & 0 deletions api/hypershift/v1beta1/hosted_controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type HostedControlPlane struct {
// HostedControlPlaneSpec defines the desired state of HostedControlPlane
// +kubebuilder:validation:XValidation:rule="self.platform.type == 'IBMCloud' ? size(self.services) >= 3 : size(self.services) >= 4",message="spec.services in body should have at least 4 items or 3 for IBMCloud"
// +kubebuilder:validation:XValidation:rule="!has(self.operatorConfiguration) || !has(self.operatorConfiguration.clusterNetworkOperator) || !has(self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork) || !self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork || self.networking.networkType == 'Other'",message="disableMultiNetwork can only be set to true when networkType is 'Other'"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.secretEncryption) || has(self.secretEncryption)",message="secretEncryption cannot be removed once configured"
type HostedControlPlaneSpec struct {
// releaseImage is the release image applied to the hosted control plane.
// +required
Expand Down Expand Up @@ -419,6 +420,10 @@ type HostedControlPlaneStatus struct {
// configuration contains the cluster configuration status of the HostedCluster
// +optional
Configuration *ConfigurationStatus `json:"configuration,omitempty"`

// secretEncryption tracks the state of secret encryption key rotation and re-encryption.
// +optional
SecretEncryption SecretEncryptionStatus `json:"secretEncryption,omitzero"`
}

// APIEndpoint represents a reachable Kubernetes API endpoint.
Expand Down
13 changes: 13 additions & 0 deletions api/hypershift/v1beta1/hostedcluster_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ const (
PublicEndpointSharedIngressConfiguredReason = "SharedIngressConfigured"
PublicEndpointTopologyPrivateReason = "TopologyPrivate"
PublicEndpointConvergenceInProgressReason = "ConvergenceInProgress"
// EtcdDataEncryptionUpToDate indicates whether all etcd data is encrypted with the
// currently active encryption key.
// True: all data confirmed encrypted with the active key.
// False: re-encryption is in progress or has failed.
// Absent: encryption is not configured.
EtcdDataEncryptionUpToDate ConditionType = "EtcdDataEncryptionUpToDate"
)

// Reasons.
Expand Down Expand Up @@ -349,6 +355,13 @@ const (
AutoNodeNotConfiguredReason = "AutoNodeNotConfigured"
AutoNodeProgressingReason = "AutoNodeProgressing"
AutoNodeEvaluationFailedReason = "AutoNodeEvaluationFailed"

ReadOnlyRolloutInProgressReason = "ReadOnlyRolloutInProgress"
WritePromotionInProgressReason = "WritePromotionInProgress"
ReEncryptionInProgressReason = "ReEncryptionInProgress"
ReEncryptionCompletedReason = "ReEncryptionCompleted"
ReEncryptionFailedReason = "ReEncryptionFailed"
ReEncryptionWaitingForKASReason = "ReEncryptionWaitingForKASConvergence"
)

// Messages.
Expand Down
142 changes: 142 additions & 0 deletions api/hypershift/v1beta1/hostedcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ type Capabilities struct {
// +kubebuilder:validation:XValidation:rule=`!self.services.exists(s, s.service == 'APIServer' && has(s.servicePublishingStrategy.loadBalancer) && s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration) && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts) && has(self.configuration.apiServer.servingCerts.namedCertificates) && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))`, message="APIServer loadBalancer hostname cannot be in ClusterConfiguration.apiserver.servingCerts.namedCertificates[]"
// +kubebuilder:validation:XValidation:rule="!has(self.operatorConfiguration) || !has(self.operatorConfiguration.clusterNetworkOperator) || !has(self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork) || !self.operatorConfiguration.clusterNetworkOperator.disableMultiNetwork || self.networking.networkType == 'Other'",message="disableMultiNetwork can only be set to true when networkType is 'Other'"
// +kubebuilder:validation:XValidation:rule="self.networking.networkType == 'OVNKubernetes' || !has(self.operatorConfiguration) || !has(self.operatorConfiguration.clusterNetworkOperator) || !has(self.operatorConfiguration.clusterNetworkOperator.ovnKubernetesConfig)", message="ovnKubernetesConfig is forbidden when networkType is not OVNKubernetes"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.secretEncryption) || has(self.secretEncryption)",message="secretEncryption cannot be removed once configured"
type HostedClusterSpec struct {
// release specifies the desired OCP release payload for all the hosted cluster components.
// This includes those components running management side like the Kube API Server and the CVO but also the operands which land in the hosted cluster data plane like the ingress controller, ovn agents, etc.
Expand Down Expand Up @@ -2073,10 +2074,147 @@ type AESCBCSpec struct {
ActiveKey corev1.LocalObjectReference `json:"activeKey"`
// backupKey defines the old key during the rotation process so previously created
// secrets can continue to be decrypted until they are all re-encrypted with the active key.
//
// Deprecated: This field will be ignored when status.secretEncryption.activeKey is set.
// The system automatically manages the previous key via the status field.
// +optional
BackupKey *corev1.LocalObjectReference `json:"backupKey,omitempty"`
}

// SecretEncryptionProvider identifies the encryption provider recorded in status.
// This is a separate type from KMSProvider because the KMSProvider enum does not include AESCBC.
type SecretEncryptionProvider string

const (
SecretEncryptionProviderAzure SecretEncryptionProvider = "Azure"
SecretEncryptionProviderAWS SecretEncryptionProvider = "AWS"
SecretEncryptionProviderIBMCloud SecretEncryptionProvider = "IBMCloud"
SecretEncryptionProviderAESCBC SecretEncryptionProvider = "AESCBC"
)

// SecretEncryptionStatus tracks the state of secret encryption key rotation and re-encryption.
// +k8s:deepcopy-gen=true
Comment thread
muraee marked this conversation as resolved.
// +kubebuilder:validation:MinProperties=1
type SecretEncryptionStatus struct {
Comment thread
muraee marked this conversation as resolved.
// activeKey is the encryption key specification that all etcd data is confirmed encrypted with.
// Updated after successful re-encryption.
// +optional
ActiveKey SecretEncryptionKeyStatus `json:"activeKey,omitzero"`
// targetKey is the key being rolled out during an active rotation. Snapshot from
// spec.secretEncryption's active key when the rotation starts. The CPO uses this
// (not the current spec) during the rotation, so mid-rotation spec changes are
// safely queued until the current rotation completes. Cleared when rotation completes.
// +optional
TargetKey SecretEncryptionKeyStatus `json:"targetKey,omitzero"`
// history contains a list of key rotations applied to this cluster. The newest
// entry is first in the list. Entries have state Completed when re-encryption
// has finished. The current rotation phase is always history[0].state when
// history[0] is not Completed or Interrupted.
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=5
History []EncryptionMigrationHistory `json:"history,omitempty"`
}

// SecretEncryptionKeyStatus records the active key identity using the same types as the spec.
// +kubebuilder:validation:XValidation:rule="self.provider == 'Azure' ? has(self.azure) : !has(self.azure)",message="azure is required when provider is Azure, and forbidden otherwise"
// +kubebuilder:validation:XValidation:rule="self.provider == 'AWS' ? has(self.aws) : !has(self.aws)",message="aws is required when provider is AWS, and forbidden otherwise"
// +kubebuilder:validation:XValidation:rule="self.provider == 'IBMCloud' ? has(self.ibmCloud) : !has(self.ibmCloud)",message="ibmCloud is required when provider is IBMCloud, and forbidden otherwise"
// +kubebuilder:validation:XValidation:rule="self.provider == 'AESCBC' ? has(self.aescbc) : !has(self.aescbc)",message="aescbc is required when provider is AESCBC, and forbidden otherwise"
// +union
type SecretEncryptionKeyStatus struct {
// provider identifies the encryption provider.
// +required
// +unionDiscriminator
// +kubebuilder:validation:Enum=Azure;AWS;IBMCloud;AESCBC
Provider SecretEncryptionProvider `json:"provider,omitempty"`
// azure holds the Azure KMS key identity fields.
// +optional
// +unionMember
Azure AzureKMSKey `json:"azure,omitzero"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does azure struct doesn't follow the same naming pattern than aws/ibm?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question. Though, I am just reusing the same struct from the spec.

// aws holds the AWS KMS key identity fields.
// +optional
// +unionMember
AWS AWSKMSKeyEntry `json:"aws,omitzero"`
// ibmCloud holds the IBM Cloud KMS key identity fields.
// +optional
// +unionMember
IBMCloud IBMCloudKMSKeyEntry `json:"ibmCloud,omitzero"`
// aescbc holds a reference to the AESCBC key secret.
// +optional
// +unionMember
AESCBC AESCBCKeyStatus `json:"aescbc,omitzero"`
}

// AESCBCKeyStatus contains a reference to the AESCBC key secret and a SHA-256 hash
// of its contents for fingerprinting.
type AESCBCKeyStatus struct {
// secret is a reference to the secret containing the AESCBC key.
// +required
Secret SecretReference `json:"secret,omitzero"`
// dataHash is the hex-encoded SHA-256 hash of the secret's "key" data field
// at the time re-encryption completed.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
DataHash string `json:"dataHash,omitempty"`
}

// EncryptionKeyReference identifies an encryption key by its provider and fingerprint.
type EncryptionKeyReference struct {
// provider identifies the encryption provider.
// +required
// +kubebuilder:validation:Enum=Azure;AWS;IBMCloud;AESCBC
Provider SecretEncryptionProvider `json:"provider,omitempty"`
// fingerprint is the hex-encoded SHA-256 hash of the key's identity fields.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
Fingerprint string `json:"fingerprint,omitempty"`
}

// EncryptionMigrationState tracks the lifecycle of a key rotation.
// +kubebuilder:validation:Enum=ReadOnlyDeploy;WritePromote;Migrating;Completed;Interrupted
type EncryptionMigrationState string

const (
// EncryptionMigrationStateReadOnlyDeploy means the new key is being deployed as a read-only
// provider. The old key remains the write provider.
EncryptionMigrationStateReadOnlyDeploy EncryptionMigrationState = "ReadOnlyDeploy"
// EncryptionMigrationStateWritePromote means the new key is being promoted to write provider.
// The old key becomes read-only.
EncryptionMigrationStateWritePromote EncryptionMigrationState = "WritePromote"
// EncryptionMigrationStateMigrating means all KAS replicas have converged on the new write
// provider and re-encryption (StorageVersionMigration) is in progress.
EncryptionMigrationStateMigrating EncryptionMigrationState = "Migrating"
// EncryptionMigrationStateCompleted means all data was successfully re-encrypted with the target key.
EncryptionMigrationStateCompleted EncryptionMigrationState = "Completed"
// EncryptionMigrationStateInterrupted means the rotation was abandoned before data was encrypted
// with the target key (e.g., targetKey replaced during ReadOnlyDeploy).
EncryptionMigrationStateInterrupted EncryptionMigrationState = "Interrupted"
)

// EncryptionMigrationHistory records a key rotation, including in-progress rotations.
// +k8s:deepcopy-gen=true
type EncryptionMigrationHistory struct {
// from is the key that data was migrated from (the previous active key).
// +required
From EncryptionKeyReference `json:"from,omitzero"`
// to is the key that data was migrated to (the target key).
// +required
To EncryptionKeyReference `json:"to,omitzero"`
// state tracks the current phase of this rotation.
// +required
State EncryptionMigrationState `json:"state,omitempty"`
Comment thread
muraee marked this conversation as resolved.
// startedTime is when the rotation was initiated.
// +required
StartedTime metav1.Time `json:"startedTime,omitzero"`
// completionTime is when the rotation finished. Not set while the rotation is in progress.
// +optional
CompletionTime metav1.Time `json:"completionTime,omitzero"`
}

type PayloadArchType string

const (
Expand Down Expand Up @@ -2190,6 +2328,10 @@ type HostedClusterStatus struct {
// +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"`

// secretEncryption tracks the state of secret encryption key rotation and re-encryption.
// +optional
SecretEncryption SecretEncryptionStatus `json:"secretEncryption,omitzero"`
}

// AutoNodeStatus contains the observed state of the AutoNode provisioner.
Expand Down
14 changes: 9 additions & 5 deletions api/hypershift/v1beta1/ibmcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,26 @@ type IBMCloudKMSSpec struct {
// IBMCloudKMSKeyEntry defines metadata for an IBM Cloud KMS encryption key
type IBMCloudKMSKeyEntry struct {
// crkID is the customer rook key id
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
CRKID string `json:"crkID"`
CRKID string `json:"crkID,omitempty"`
// instanceID is the id for the key protect instance
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
InstanceID string `json:"instanceID"`
InstanceID string `json:"instanceID,omitempty"`
// correlationID is an identifier used to track all api call usage from hypershift
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=255
// +required
CorrelationID string `json:"correlationID"`
CorrelationID string `json:"correlationID,omitempty"`
// url is the url to call key protect apis over
// +kubebuilder:validation:Pattern=`^https://`
// +kubebuilder:validation:XValidation:rule="self.startsWith('https://')",message="url must start with 'https://'"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=2048
// +required
URL string `json:"url"`
URL string `json:"url,omitempty"`
// keyVersion is a unique number associated with the key. The number increments whenever a new
// key is enabled for data encryption.
// +kubebuilder:validation:Minimum=0
Expand Down
Loading