Skip to content
Open
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
40 changes: 40 additions & 0 deletions api/hypershift/v1beta1/hosted_controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,12 @@ type HostedControlPlaneStatus struct {
// +optional
Configuration *ConfigurationStatus `json:"configuration,omitempty"`

// operatorConfiguration reports which settings from spec.operatorConfiguration the
// reconciling control plane operator understands. It lets a newer HyperShift operator
// detect settings that an older control plane operator silently ignores.
// +optional
OperatorConfiguration OperatorConfigurationStatus `json:"operatorConfiguration,omitzero"`

// secretEncryption tracks the state of secret encryption key rotation and re-encryption.
// +optional
SecretEncryption SecretEncryptionStatus `json:"secretEncryption,omitzero"`
Expand All @@ -436,6 +442,40 @@ type HostedControlPlaneStatus struct {
Initialization HostedControlPlaneInitializationStatus `json:"initialization,omitzero"`
}

// OperatorConfigurationStatus reports how the reconciling control plane operator
// interpreted spec.operatorConfiguration.
//
// A control plane operator only deserializes the spec fields its own build knows about;
// the API server serves settings added in later releases, but they are dropped on decode
// and silently ignored. Echoing back the settings that survived that decode lets the
// HyperShift operator name the settings a lagging control plane operator does not honor,
// without either side needing per-field knowledge of the other.
// +kubebuilder:validation:MinProperties=1
type OperatorConfigurationStatus struct {
// observedGeneration is the HostedControlPlane generation whose
// spec.operatorConfiguration produced honoredFields. A value lower than
// metadata.generation means the control plane operator has not yet observed the
// latest spec, so honoredFields is stale.
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=9007199254740992
ObservedGeneration int64 `json:"observedGeneration,omitempty,omitzero"`

// honoredFields lists the dot-separated paths of the settings that are both set in
// spec.operatorConfiguration and understood by the reconciling control plane operator,
// relative to spec.operatorConfiguration and sorted lexically
// (for example "kubeAPIServer.eventTTLMinutes"). It is empty when no settings are
// configured. A setting that is set in the spec but absent from this list is not in
// effect.
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=200
// +kubebuilder:validation:items:MinLength=1
// +kubebuilder:validation:items:MaxLength=253
HonoredFields []string `json:"honoredFields,omitempty"`
}

// HostedControlPlaneInitializationStatus provides observations of the HostedControlPlane initialization process.
// This satisfies the CAPI v1beta2 ControlPlane provider contract:
// https://github.com/kubernetes-sigs/cluster-api/blob/v1.11.5/api/core/v1beta2/cluster_types.go#L1361-L1379
Expand Down
109 changes: 109 additions & 0 deletions api/hypershift/v1beta1/hosted_controlplane_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package v1beta1

import (
"encoding/json"
"reflect"
"testing"
)

// hostedControlPlaneStatusNMinus1 is a shape probe for the N-1 version of
// HostedControlPlaneStatus, before the operatorConfiguration field was added. Consumers
// such as ARO-HCP embed these types and serialize them to their own storage outside CRD
// validation, so a rollback has to be able to read data written by the newer type.
type hostedControlPlaneStatusNMinus1 struct {
// ready reports whether the control plane is ready.
Ready bool `json:"ready"` //nolint:kubeapilinter // test-only N-1 compat struct
// initialized reports whether the control plane is initialized.
Initialized bool `json:"initialized"` //nolint:kubeapilinter // test-only N-1 compat struct
}

func TestOperatorConfigurationStatusSerializationCompatibility(t *testing.T) {
tests := []struct {
name string
// current is the N (current) version of the struct.
current OperatorConfigurationStatus
// expectedJSON is the expected JSON output from marshaling current.
expectedJSON string
}{
{
name: "When nothing has been reported it should serialize to an empty object",
current: OperatorConfigurationStatus{},
expectedJSON: `{}`,
},
{
name: "When a generation is observed with no settings configured it should omit honoredFields",
current: OperatorConfigurationStatus{
ObservedGeneration: 4,
},
expectedJSON: `{"observedGeneration":4}`,
},
{
name: "When settings are honored it should serialize both fields",
current: OperatorConfigurationStatus{
ObservedGeneration: 7,
HonoredFields: []string{"etcd.logLevel", "kubeAPIServer.eventTTLMinutes"},
},
expectedJSON: `{"observedGeneration":7,"honoredFields":["etcd.logLevel","kubeAPIServer.eventTTLMinutes"]}`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
encoded, err := json.Marshal(tt.current)
if err != nil {
t.Fatalf("failed to marshal current type: %v", err)
}
if string(encoded) != tt.expectedJSON {
t.Errorf("unexpected JSON:\n got: %s\nwant: %s", encoded, tt.expectedJSON)
}

// N (forward): the current type reads back what it wrote.
var roundTripped OperatorConfigurationStatus
if err := json.Unmarshal(encoded, &roundTripped); err != nil {
t.Fatalf("failed to unmarshal into current type: %v", err)
}
if !reflect.DeepEqual(roundTripped, tt.current) {
t.Errorf("round trip changed the value:\n got: %#v\nwant: %#v", roundTripped, tt.current)
}
})
}
}

func TestHostedControlPlaneStatusSerializationCompatibility(t *testing.T) {
// N-1 (rollback): a consumer without the operatorConfiguration field must still be
// able to deserialize a status written by the current type.
current := HostedControlPlaneStatus{
Ready: true,
Initialized: true,
OperatorConfiguration: OperatorConfigurationStatus{
ObservedGeneration: 7,
HonoredFields: []string{"kubeAPIServer.eventTTLMinutes"},
},
}
encoded, err := json.Marshal(current)
if err != nil {
t.Fatalf("failed to marshal current type: %v", err)
}

var nMinus1 hostedControlPlaneStatusNMinus1
if err := json.Unmarshal(encoded, &nMinus1); err != nil {
t.Fatalf("N-1 consumer failed to deserialize status written by the current type: %v", err)
}
if want := (hostedControlPlaneStatusNMinus1{Ready: true, Initialized: true}); nMinus1 != want {
t.Errorf("N-1 consumer read unexpected values:\n got: %#v\nwant: %#v", nMinus1, want)
}

// N+1 (forward): the current type must read a status written before the field
// existed, and see the zero value rather than failing.
olderEncoded, err := json.Marshal(hostedControlPlaneStatusNMinus1{Ready: true, Initialized: true})
if err != nil {
t.Fatalf("failed to marshal N-1 type: %v", err)
}
var fromOlder HostedControlPlaneStatus
if err := json.Unmarshal(olderEncoded, &fromOlder); err != nil {
t.Fatalf("current type failed to deserialize status written by an N-1 consumer: %v", err)
}
if !reflect.DeepEqual(fromOlder.OperatorConfiguration, OperatorConfigurationStatus{}) {
t.Errorf("expected the zero OperatorConfigurationStatus from an N-1 status, got: %#v", fromOlder.OperatorConfiguration)
}
}
36 changes: 31 additions & 5 deletions api/hypershift/v1beta1/hostedcluster_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,24 @@ const (
// keeps serving and the HostedCluster does not become degraded.
// The condition is absent when no defaultCertificate is configured.
IngressDefaultCertificateSynced ConditionType = "IngressDefaultCertificateSynced"

// OperatorConfigurationHonored indicates whether every setting configured under
// spec.operatorConfiguration is actually in effect on the hosted control plane.
// It is intentionally generic so that one condition covers the whole stanza and
// every setting added to it; the message names the specific settings at issue.
//
// A control plane operator older than the HyperShift operator accepts and persists
// a setting it predates, then ignores it. This condition is how that is reported.
//
// **True** (reason AsExpected) means no setting is configured, or every configured
// setting is honored by the reconciling control plane operator.
// **False** (reason OperatorConfigurationNotHonored) means at least one configured
// setting is not in effect, and the message names them.
// **Unknown** (reason UnsupportedControlPlaneVersion or StatusUnknown) means whether
// the configured settings are honored could not be determined, because the control
// plane operator predates this reporting, its version is unknown, or it has not yet
// observed the current spec.
OperatorConfigurationHonored ConditionType = "OperatorConfigurationHonored"
)

// Reasons for PublicEndpointExposed condition.
Expand Down Expand Up @@ -341,11 +359,19 @@ const (
// imply the mechanism is currently driving configuration, since a non-deprecated
// field may take precedence over it.
DeprecatedConfigurationInUseReason = "DeprecatedConfigurationInUse"
NotFoundReason = "NotFound"
WaitingForAvailableReason = "WaitingForAvailable"
SecretNotFoundReason = "SecretNotFound"
WaitingForGracePeriodReason = "WaitingForGracePeriod"
BlockedReason = "Blocked"
// OperatorConfigurationNotHonoredReason is used with the OperatorConfigurationHonored
// condition when at least one setting configured under spec.operatorConfiguration is
// not in effect because the reconciling control plane operator does not support it.
OperatorConfigurationNotHonoredReason = "OperatorConfigurationNotHonored"
// UnsupportedControlPlaneVersionReason is used when the reconciling control plane
// operator is too old to report the information a condition is derived from, so the
// condition cannot be evaluated.
UnsupportedControlPlaneVersionReason = "UnsupportedControlPlaneVersion"
NotFoundReason = "NotFound"
WaitingForAvailableReason = "WaitingForAvailable"
SecretNotFoundReason = "SecretNotFound"
WaitingForGracePeriodReason = "WaitingForGracePeriod"
BlockedReason = "Blocked"

InfraStatusFailureReason = "InfraStatusFailure"
WaitingOnInfrastructureReadyReason = "WaitingOnInfrastructureReady"
Expand Down
21 changes: 21 additions & 0 deletions api/hypershift/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7343,6 +7343,40 @@ spec:
This is populated after the infrastructure is ready.
maxLength: 255
type: string
operatorConfiguration:
description: |-
operatorConfiguration reports which settings from spec.operatorConfiguration the
reconciling control plane operator understands. It lets a newer HyperShift operator
detect settings that an older control plane operator silently ignores.
minProperties: 1
properties:
honoredFields:
description: |-
honoredFields lists the dot-separated paths of the settings that are both set in
spec.operatorConfiguration and understood by the reconciling control plane operator,
relative to spec.operatorConfiguration and sorted lexically
(for example "kubeAPIServer.eventTTLMinutes"). It is empty when no settings are
configured. A setting that is set in the spec but absent from this list is not in
effect.
items:
maxLength: 253
minLength: 1
type: string
maxItems: 200
minItems: 1
type: array
x-kubernetes-list-type: atomic
observedGeneration:
description: |-
observedGeneration is the HostedControlPlane generation whose
spec.operatorConfiguration produced honoredFields. A value lower than
metadata.generation means the control plane operator has not yet observed the
latest spec, so honoredFields is stale.
format: int64
maximum: 9007199254740992
minimum: 1
type: integer
type: object
platform:
description: platform contains platform-specific status of the HostedCluster
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7390,6 +7390,40 @@ spec:
This is populated after the infrastructure is ready.
maxLength: 255
type: string
operatorConfiguration:
description: |-
operatorConfiguration reports which settings from spec.operatorConfiguration the
reconciling control plane operator understands. It lets a newer HyperShift operator
detect settings that an older control plane operator silently ignores.
minProperties: 1
properties:
honoredFields:
description: |-
honoredFields lists the dot-separated paths of the settings that are both set in
spec.operatorConfiguration and understood by the reconciling control plane operator,
relative to spec.operatorConfiguration and sorted lexically
(for example "kubeAPIServer.eventTTLMinutes"). It is empty when no settings are
configured. A setting that is set in the spec but absent from this list is not in
effect.
items:
maxLength: 253
minLength: 1
type: string
maxItems: 200
minItems: 1
type: array
x-kubernetes-list-type: atomic
observedGeneration:
description: |-
observedGeneration is the HostedControlPlane generation whose
spec.operatorConfiguration produced honoredFields. A value lower than
metadata.generation means the control plane operator has not yet observed the
latest spec, so honoredFields is stale.
format: int64
maximum: 9007199254740992
minimum: 1
type: integer
type: object
platform:
description: platform contains platform-specific status of the HostedCluster
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7326,6 +7326,40 @@ spec:
This is populated after the infrastructure is ready.
maxLength: 255
type: string
operatorConfiguration:
description: |-
operatorConfiguration reports which settings from spec.operatorConfiguration the
reconciling control plane operator understands. It lets a newer HyperShift operator
detect settings that an older control plane operator silently ignores.
minProperties: 1
properties:
honoredFields:
description: |-
honoredFields lists the dot-separated paths of the settings that are both set in
spec.operatorConfiguration and understood by the reconciling control plane operator,
relative to spec.operatorConfiguration and sorted lexically
(for example "kubeAPIServer.eventTTLMinutes"). It is empty when no settings are
configured. A setting that is set in the spec but absent from this list is not in
effect.
items:
maxLength: 253
minLength: 1
type: string
maxItems: 200
minItems: 1
type: array
x-kubernetes-list-type: atomic
observedGeneration:
description: |-
observedGeneration is the HostedControlPlane generation whose
spec.operatorConfiguration produced honoredFields. A value lower than
metadata.generation means the control plane operator has not yet observed the
latest spec, so honoredFields is stale.
format: int64
maximum: 9007199254740992
minimum: 1
type: integer
type: object
platform:
description: platform contains platform-specific status of the HostedCluster
properties:
Expand Down
Loading