diff --git a/pkg/api/admin/openshiftcluster.go b/pkg/api/admin/openshiftcluster.go index d8e6fdb1f40..c220fea9fd6 100644 --- a/pkg/api/admin/openshiftcluster.go +++ b/pkg/api/admin/openshiftcluster.go @@ -34,6 +34,7 @@ type OpenShiftClusterProperties struct { FailedProvisioningState ProvisioningState `json:"failedProvisioningState,omitempty"` LastAdminUpdateError string `json:"lastAdminUpdateError,omitempty"` MaintenanceTask MaintenanceTask `json:"maintenanceTask,omitempty" mutable:"true"` + MaintenanceState MaintenanceState `json:"maintenanceState,omitempty" mutable:"true"` OperatorFlags OperatorFlags `json:"operatorFlags,omitempty" mutable:"true"` OperatorVersion string `json:"operatorVersion,omitempty" mutable:"true"` CreatedAt time.Time `json:"createdAt,omitempty"` @@ -81,9 +82,21 @@ const ( type MaintenanceTask string const ( - MaintenanceTaskEverything MaintenanceTask = "Everything" - MaintenanceTaskOperator MaintenanceTask = "OperatorUpdate" - MaintenanceTaskRenewCerts MaintenanceTask = "CertificatesRenewal" + MaintenanceTaskEverything MaintenanceTask = "Everything" + MaintenanceTaskOperator MaintenanceTask = "OperatorUpdate" + MaintenanceTaskRenewCerts MaintenanceTask = "CertificatesRenewal" + MaintenanceTaskStateUpdate MaintenanceTask = "MaintenanceStateUpdate" +) + +// MaintenanceState represents a maintenance state +type MaintenanceState string + +// MaintenanceState constants +const ( + MaintenanceStateNone MaintenanceState = "None" + MaintenanceStatePending MaintenanceState = "Pending" + MaintenanceStatePlanned MaintenanceState = "Planned" + MaintenanceStateUnplanned MaintenanceState = "Unplanned" ) // Operator feature flags diff --git a/pkg/api/admin/openshiftcluster_convert.go b/pkg/api/admin/openshiftcluster_convert.go index 2dfa4962f74..31b8276c21c 100644 --- a/pkg/api/admin/openshiftcluster_convert.go +++ b/pkg/api/admin/openshiftcluster_convert.go @@ -26,6 +26,7 @@ func (c openShiftClusterConverter) ToExternal(oc *api.OpenShiftCluster) interfac FailedProvisioningState: ProvisioningState(oc.Properties.FailedProvisioningState), LastAdminUpdateError: oc.Properties.LastAdminUpdateError, MaintenanceTask: MaintenanceTask(oc.Properties.MaintenanceTask), + MaintenanceState: MaintenanceState(oc.Properties.MaintenanceState), OperatorFlags: OperatorFlags(oc.Properties.OperatorFlags), OperatorVersion: oc.Properties.OperatorVersion, CreatedAt: oc.Properties.CreatedAt, @@ -173,6 +174,7 @@ func (c openShiftClusterConverter) ToInternal(_oc interface{}, out *api.OpenShif out.Properties.FailedProvisioningState = api.ProvisioningState(oc.Properties.FailedProvisioningState) out.Properties.LastAdminUpdateError = oc.Properties.LastAdminUpdateError out.Properties.MaintenanceTask = api.MaintenanceTask(oc.Properties.MaintenanceTask) + out.Properties.MaintenanceState = api.MaintenanceState(oc.Properties.MaintenanceState) out.Properties.OperatorFlags = api.OperatorFlags(oc.Properties.OperatorFlags) out.Properties.OperatorVersion = oc.Properties.OperatorVersion out.Properties.CreatedBy = oc.Properties.CreatedBy diff --git a/pkg/api/admin/openshiftcluster_validatestatic.go b/pkg/api/admin/openshiftcluster_validatestatic.go index cad10cbf2c9..fb2c2b0f40a 100644 --- a/pkg/api/admin/openshiftcluster_validatestatic.go +++ b/pkg/api/admin/openshiftcluster_validatestatic.go @@ -29,9 +29,47 @@ func (sv openShiftClusterStaticValidator) validateDelta(oc, current *OpenShiftCl return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodePropertyChangeNotAllowed, err.Target, err.Message) } - if !(oc.Properties.MaintenanceTask == "" || oc.Properties.MaintenanceTask == MaintenanceTaskEverything || oc.Properties.MaintenanceTask == MaintenanceTaskOperator || oc.Properties.MaintenanceTask == MaintenanceTaskRenewCerts) { + if invalidMaintenanceTask(oc.Properties.MaintenanceTask) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "properties.maintenanceTask", "Invalid enum parameter.") } + if invalidMaintenanceState(oc.Properties.MaintenanceState) { + return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "properties.maintenanceState", "Invalid enum parameter.") + } + return nil } + +func invalidMaintenanceTask(task MaintenanceTask) bool { + switch task { + case "": + fallthrough + case MaintenanceTaskEverything: + fallthrough + case MaintenanceTaskOperator: + fallthrough + case MaintenanceTaskRenewCerts: + fallthrough + case MaintenanceTaskStateUpdate: + return false + } + + return true +} + +func invalidMaintenanceState(state MaintenanceState) bool { + switch state { + case "": + fallthrough + case MaintenanceStateNone: + fallthrough + case MaintenanceStatePending: + fallthrough + case MaintenanceStatePlanned: + fallthrough + case MaintenanceStateUnplanned: + return false + } + + return true +} diff --git a/pkg/api/admin/openshiftcluster_validatestatic_test.go b/pkg/api/admin/openshiftcluster_validatestatic_test.go index a9ce5135655..a90560238ea 100644 --- a/pkg/api/admin/openshiftcluster_validatestatic_test.go +++ b/pkg/api/admin/openshiftcluster_validatestatic_test.go @@ -690,6 +690,85 @@ func TestOpenShiftClusterStaticValidateDelta(t *testing.T) { }, wantErr: "400: InvalidParameter: properties.maintenanceTask: Invalid enum parameter.", }, + { + name: "maintenanceState change to blank allowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: MaintenanceStateNone, + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = "" + }, + }, + { + name: "maintenanceState change to none allowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: "", + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = MaintenanceStateNone + }, + }, + { + name: "maintenanceState change to pending allowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: MaintenanceStateNone, + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = MaintenanceStatePending + }, + }, + { + name: "maintenanceState change to planned allowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: MaintenanceStateNone, + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = MaintenanceStatePlanned + }, + }, + { + name: "maintenanceState change to unplanned allowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: MaintenanceStateNone, + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = MaintenanceStateUnplanned + }, + }, + { + name: "maintenanceState change to other values is disallowed", + oc: func() *OpenShiftCluster { + return &OpenShiftCluster{ + Properties: OpenShiftClusterProperties{ + MaintenanceState: MaintenanceStateNone, + }, + } + }, + modify: func(oc *OpenShiftCluster) { + oc.Properties.MaintenanceState = "adfasdfadf" + }, + wantErr: "400: InvalidParameter: properties.maintenanceState: Invalid enum parameter.", + }, } for _, tt := range tests { diff --git a/pkg/api/defaults.go b/pkg/api/defaults.go index 355b691ab8f..1a0cdb357ee 100644 --- a/pkg/api/defaults.go +++ b/pkg/api/defaults.go @@ -46,6 +46,11 @@ func SetDefaults(doc *OpenShiftClusterDocument) { if doc.OpenShiftCluster.Properties.NetworkProfile.PreconfiguredNSG == "" { doc.OpenShiftCluster.Properties.NetworkProfile.PreconfiguredNSG = PreconfiguredNSGDisabled } + + // If there's no maintenance states, set to none + if doc.OpenShiftCluster.Properties.MaintenanceState == "" { + doc.OpenShiftCluster.Properties.MaintenanceState = MaintenanceStateNone + } } } diff --git a/pkg/api/openshiftcluster.go b/pkg/api/openshiftcluster.go index 4201c544f15..dadcb178de3 100644 --- a/pkg/api/openshiftcluster.go +++ b/pkg/api/openshiftcluster.go @@ -100,6 +100,7 @@ type OpenShiftClusterProperties struct { FailedProvisioningState ProvisioningState `json:"failedProvisioningState,omitempty"` LastAdminUpdateError string `json:"lastAdminUpdateError,omitempty"` MaintenanceTask MaintenanceTask `json:"maintenanceTask,omitempty"` + MaintenanceState MaintenanceState `json:"maintenanceState,omitempty"` // Operator feature/option flags OperatorFlags OperatorFlags `json:"operatorFlags,omitempty"` @@ -175,9 +176,21 @@ const ( type MaintenanceTask string const ( - MaintenanceTaskEverything MaintenanceTask = "Everything" - MaintenanceTaskOperator MaintenanceTask = "OperatorUpdate" - MaintenanceTaskRenewCerts MaintenanceTask = "CertificatesRenewal" + MaintenanceTaskEverything MaintenanceTask = "Everything" + MaintenanceTaskOperator MaintenanceTask = "OperatorUpdate" + MaintenanceTaskRenewCerts MaintenanceTask = "CertificatesRenewal" + MaintenanceTaskStateUpdate MaintenanceTask = "MaintenanceStateUpdate" +) + +// MaintenanceState represents a maintenance state +type MaintenanceState string + +// MaintenanceState constants +const ( + MaintenanceStateNone MaintenanceState = "None" + MaintenanceStatePending MaintenanceState = "Pending" + MaintenanceStatePlanned MaintenanceState = "Planned" + MaintenanceStateUnplanned MaintenanceState = "Unplanned" ) // Cluster-scoped flags @@ -192,6 +205,10 @@ func (t ProvisioningState) String() string { return string(t) } +func (t MaintenanceState) String() string { + return string(t) +} + // FipsValidatedModules determines if FIPS is used. type FipsValidatedModules string diff --git a/pkg/cluster/adminupdate_test.go b/pkg/cluster/adminupdate_test.go index 557f23bcdae..77b1c0e8f5e 100644 --- a/pkg/cluster/adminupdate_test.go +++ b/pkg/cluster/adminupdate_test.go @@ -252,6 +252,24 @@ func TestAdminUpdateSteps(t *testing.T) { "[Action updateProvisionedBy-fm]", }, }, + { + name: "Maintenance State Update", + fixture: func() (*api.OpenShiftClusterDocument, bool) { + doc := baseClusterDoc() + doc.OpenShiftCluster.Properties.ProvisioningState = api.ProvisioningStateAdminUpdating + doc.OpenShiftCluster.Properties.MaintenanceTask = api.MaintenanceTaskStateUpdate + return doc, true + }, + shouldRunSteps: []string{ + "[Action initializeKubernetesClients-fm]", + "[Action ensureBillingRecord-fm]", + "[Action ensureDefaults-fm]", + "[AuthorizationRetryingAction fixupClusterSPObjectID-fm]", + "[Action fixInfraID-fm]", + "[Action startVMs-fm]", + "[Condition apiServersReady-fm, timeout 30m0s]", + }, + }, } { t.Run(tt.name, func(t *testing.T) { doc, adoptViaHive := tt.fixture() diff --git a/pkg/monitor/cluster/cluster.go b/pkg/monitor/cluster/cluster.go index b4e934bcf53..ecf3448e626 100644 --- a/pkg/monitor/cluster/cluster.go +++ b/pkg/monitor/cluster/cluster.go @@ -170,6 +170,7 @@ func (mon *Monitor) Monitor(ctx context.Context) (errs []error) { mon.emitSummary, mon.emitHiveRegistrationStatus, mon.emitOperatorFlagsAndSupportBanner, + mon.emitClusterMaintenanceState, mon.emitPrometheusAlerts, // at the end for now because it's the slowest/least reliable } { err = f(ctx) diff --git a/pkg/monitor/cluster/clustermaintenancestate.go b/pkg/monitor/cluster/clustermaintenancestate.go new file mode 100644 index 00000000000..7c30f0843e0 --- /dev/null +++ b/pkg/monitor/cluster/clustermaintenancestate.go @@ -0,0 +1,14 @@ +package cluster + +import "context" + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +func (mon *Monitor) emitClusterMaintenanceState(ctx context.Context) error { + mon.emitGauge("cluster.maintenanceState", 1, map[string]string{ + "maintenanceState": mon.oc.Properties.MaintenanceState.String(), + }) + + return nil +} diff --git a/pkg/monitor/cluster/clustermaintenancestate_test.go b/pkg/monitor/cluster/clustermaintenancestate_test.go new file mode 100644 index 00000000000..338d5d94699 --- /dev/null +++ b/pkg/monitor/cluster/clustermaintenancestate_test.go @@ -0,0 +1,41 @@ +package cluster + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/Azure/ARO-RP/pkg/api" + mock_metrics "github.com/Azure/ARO-RP/pkg/util/mocks/metrics" +) + +func TestEmitClusterMaintenanceState(t *testing.T) { + ctx := context.Background() + + controller := gomock.NewController(t) + defer controller.Finish() + + m := mock_metrics.NewMockEmitter(controller) + + mon := &Monitor{ + m: m, + oc: &api.OpenShiftCluster{ + Properties: api.OpenShiftClusterProperties{ + MaintenanceState: api.MaintenanceStateNone, + }, + }, + } + + m.EXPECT().EmitGauge("cluster.maintenanceState", int64(1), map[string]string{ + "maintenanceState": "None", + }) + + err := mon.emitClusterMaintenanceState(ctx) + if err != nil { + t.Fatal(err) + } +}