From ebb6a4edd9ff55e7deed3976f8925c41b380fae9 Mon Sep 17 00:00:00 2001 From: rthalho Date: Tue, 16 Jan 2024 08:32:30 +0100 Subject: [PATCH 1/5] feat: add sample kube-state-metrics-config --- config/kube-state-metrics/configmap.yaml | 329 +++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 config/kube-state-metrics/configmap.yaml diff --git a/config/kube-state-metrics/configmap.yaml b/config/kube-state-metrics/configmap.yaml new file mode 100644 index 00000000..fff61395 --- /dev/null +++ b/config/kube-state-metrics/configmap.yaml @@ -0,0 +1,329 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + labels: + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/component: metrics + name: kube-state-metrics-config + namespace: system +data: + config.yaml: | + kind: CustomResourceStateMetrics + spec: + resources: + - commonLabels: + crd_type: enterprise + groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: Enterprise + version: v1alpha1 + labelsFromPath: + name: + - metadata + - name + metricNamePrefix: garm_operator + metrics: + - each: + gauge: + path: + - metadata + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: enterprise_created + - each: + gauge: + nilIsZero: true + path: + - status + - poolManagerIsRunning + type: Gauge + help: Whether the enterprises poolManager is running. + name: enterprise_pool_manager_running + - each: + info: + labelsFromPath: + credentialsName: + - spec + - credentialsName + id: + - status + - id + webhookSecretRefKey: + - spec + - webhookSecretRef + - key + webhookSecretRefName: + - spec + - webhookSecretRef + - name + type: Info + help: Information about an enterprise. + name: enterprise_info + - each: + info: + labelsFromPath: + paused_value: [] + path: + - metadata + - annotations + - garm-operator.mercedes-benz.com/paused + type: Info + help: Whether the enterprise reconciliation is paused. + name: enterprise_annotation_paused_info + namespace: + - metadata + - namespace + - commonLabels: + crd_type: organization + groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: Organization + version: v1alpha1 + labelsFromPath: + name: + - metadata + - name + metricNamePrefix: garm_operator + metrics: + - each: + gauge: + path: + - metadata + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: org_created + - each: + gauge: + nilIsZero: true + path: + - status + - poolManagerIsRunning + type: Gauge + help: Whether the orgs poolManager is running. + name: org_pool_manager_running + - each: + info: + labelsFromPath: + credentialsName: + - spec + - credentialsName + id: + - status + - id + webhookSecretRefKey: + - spec + - webhookSecretRef + - key + webhookSecretRefName: + - spec + - webhookSecretRef + - name + type: Info + help: Information about an enterprise. + name: org_info + - each: + info: + labelsFromPath: + paused_value: [] + path: + - metadata + - annotations + - garm-operator.mercedes-benz.com/paused + type: Info + help: Whether the org reconciliation is paused. + name: org_annotation_paused_info + namespace: + - metadata + - namespace + - commonLabels: + crd_type: repository + groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: Repository + version: v1alpha1 + labelsFromPath: + name: + - metadata + - name + metricNamePrefix: garm_operator + metrics: + - each: + gauge: + path: + - metadata + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: repo_created + - each: + gauge: + nilIsZero: true + path: + - status + - poolManagerIsRunning + type: Gauge + help: Whether the repositories poolManager is running. + name: repo_pool_manager_running + - each: + info: + labelsFromPath: + credentialsName: + - spec + - credentialsName + id: + - status + - id + owner: + - spec + - owner + webhookSecretRefKey: + - spec + - webhookSecretRef + - key + webhookSecretRefName: + - spec + - webhookSecretRef + - name + type: Info + help: Information about a repository. + name: repo_info + - each: + info: + labelsFromPath: + paused_value: [] + path: + - metadata + - annotations + - garm-operator.mercedes-benz.com/paused + type: Info + help: Whether the repo reconciliation is paused. + name: repo_annotation_paused_info + namespace: + - metadata + - namespace + - commonLabels: + crd_type: pool + groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: Pool + version: v1alpha1 + labelsFromPath: + name: + - metadata + - name + metricNamePrefix: garm_operator + metrics: + - each: + gauge: + path: + - metadata + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: pool_created + - each: + gauge: + path: + - status + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: pool_min_idle_runner + - each: + info: + labelsFromPath: + enabled: + - spec + - enabled + githubRunnerGroup: + - spec + - githubRunnerGroup + id: + - status + - id + imageName: + - spec + - imageName + maxRunners: + - spec + - maxRunners + minIdleRunners: + - spec + - minIdleRunners + osArch: + - spec + - osArch + osType: + - spec + - osType + providerName: + - spec + - providerName + runnerBootstrapTimeout: + - spec + - runnerBootstrapTimeout + runnerPrefix: + - spec + - runnerPrefix + scopeKind: + - spec + - githubScopeRef + - kind + scopeName: + - spec + - githubScopeRef + - name + tags: + - spec + - tags + type: Info + help: Information about a pool. + name: pool_info + - each: + info: + labelsFromPath: + paused_value: [] + path: + - metadata + - annotations + - garm-operator.mercedes-benz.com/paused + type: Info + help: Whether the pool reconciliation is paused. + name: pool_annotation_paused_info + namespace: + - metadata + - namespace + - commonLabels: + crd_type: image + groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: Image + version: v1alpha1 + labelsFromPath: + name: + - metadata + - name + metricNamePrefix: garm_operator + metrics: + - each: + gauge: + path: + - metadata + - creationTimestamp + type: Gauge + help: Unix creation timestamp. + name: image_created + - each: + info: + labelsFromPath: + tag: + - spec + - tag + type: Info + help: Information about an image. + name: image_info + namespace: + - metadata + - namespace From 95633343b95e5a149d6c46beb749bc11127fc5a6 Mon Sep 17 00:00:00 2001 From: rthalho Date: Tue, 27 Feb 2024 08:41:58 +0100 Subject: [PATCH 2/5] feat: add kube-state-metrics docs and update sample configmap.yaml --- config/kube-state-metrics/configmap.yaml | 385 +++++++----------- .../kube-state-metrics-config.md | 96 +++++ 2 files changed, 248 insertions(+), 233 deletions(-) create mode 100644 docs/kube-state-metrics/kube-state-metrics-config.md diff --git a/config/kube-state-metrics/configmap.yaml b/config/kube-state-metrics/configmap.yaml index fff61395..22f39f51 100644 --- a/config/kube-state-metrics/configmap.yaml +++ b/config/kube-state-metrics/configmap.yaml @@ -11,319 +11,238 @@ data: kind: CustomResourceStateMetrics spec: resources: - - commonLabels: - crd_type: enterprise - groupVersionKind: + - groupVersionKind: group: garm-operator.mercedes-benz.com - kind: Enterprise - version: v1alpha1 - labelsFromPath: - name: - - metadata - - name + kind: "Enterprise" + version: "v1alpha1" metricNamePrefix: garm_operator + commonLabels: + crd_type: "enterprise" + labelsFromPath: + name: [ metadata, name ] + namespace: [ metadata, namespace ] metrics: - - each: + - name: enterprise_created + help: Unix creation timestamp. + each: gauge: path: - metadata - creationTimestamp type: Gauge - help: Unix creation timestamp. - name: enterprise_created - - each: + + - name: enterprise_pool_manager_running + help: Whether the enterprises poolManager is running. + each: gauge: nilIsZero: true - path: - - status - - poolManagerIsRunning + path: [ status, poolManagerIsRunning ] type: Gauge - help: Whether the enterprises poolManager is running. - name: enterprise_pool_manager_running - - each: + + - name: enterprise_info + help: Information about an enterprise. + each: + type: Info info: labelsFromPath: - credentialsName: - - spec - - credentialsName - id: - - status - - id - webhookSecretRefKey: - - spec - - webhookSecretRef - - key - webhookSecretRefName: - - spec - - webhookSecretRef - - name + credentialsName: [ spec, credentialsName ] + webhookSecretRefKey: [ spec, webhookSecretRef, key ] + webhookSecretRefName: [ spec, webhookSecretRef, name ] + id: [ status, id ] + + - name: enterprise_annotation_paused_info + help: Whether the enterprise reconciliation is paused. + each: type: Info - help: Information about an enterprise. - name: enterprise_info - - each: info: - labelsFromPath: - paused_value: [] path: - metadata - annotations - garm-operator.mercedes-benz.com/paused - type: Info - help: Whether the enterprise reconciliation is paused. - name: enterprise_annotation_paused_info - namespace: - - metadata - - namespace - - commonLabels: - crd_type: organization - groupVersionKind: + labelsFromPath: + paused_value: [] + + - groupVersionKind: group: garm-operator.mercedes-benz.com - kind: Organization - version: v1alpha1 - labelsFromPath: - name: - - metadata - - name + kind: "Organization" + version: "v1alpha1" metricNamePrefix: garm_operator + commonLabels: + crd_type: "organization" + labelsFromPath: + name: [ metadata, name ] + namespace: [ metadata, namespace ] metrics: - - each: + - name: org_created + help: Unix creation timestamp. + each: gauge: path: - metadata - creationTimestamp type: Gauge - help: Unix creation timestamp. - name: org_created - - each: + + - name: org_pool_manager_running + help: Whether the orgs poolManager is running. + each: gauge: nilIsZero: true - path: - - status - - poolManagerIsRunning + path: [ status, poolManagerIsRunning ] type: Gauge - help: Whether the orgs poolManager is running. - name: org_pool_manager_running - - each: + + - name: org_info + help: Information about an organization. + each: + type: Info info: labelsFromPath: - credentialsName: - - spec - - credentialsName - id: - - status - - id - webhookSecretRefKey: - - spec - - webhookSecretRef - - key - webhookSecretRefName: - - spec - - webhookSecretRef - - name + credentialsName: [ spec, credentialsName ] + webhookSecretRefKey: [ spec, webhookSecretRef, key ] + webhookSecretRefName: [ spec, webhookSecretRef, name ] + id: [ status, id ] + + - name: org_annotation_paused_info + help: Whether the org reconciliation is paused. + each: type: Info - help: Information about an enterprise. - name: org_info - - each: info: - labelsFromPath: - paused_value: [] path: - metadata - annotations - garm-operator.mercedes-benz.com/paused - type: Info - help: Whether the org reconciliation is paused. - name: org_annotation_paused_info - namespace: - - metadata - - namespace - - commonLabels: - crd_type: repository - groupVersionKind: + labelsFromPath: + paused_value: [ ] + + - groupVersionKind: group: garm-operator.mercedes-benz.com - kind: Repository - version: v1alpha1 - labelsFromPath: - name: - - metadata - - name + kind: "Repository" + version: "v1alpha1" metricNamePrefix: garm_operator + commonLabels: + crd_type: "repository" + labelsFromPath: + name: [ metadata, name ] + namespace: [ metadata, namespace ] metrics: - - each: + - name: repo_created + help: Unix creation timestamp. + each: gauge: path: - metadata - creationTimestamp type: Gauge - help: Unix creation timestamp. - name: repo_created - - each: + + - name: repo_pool_manager_running + help: Whether the repositories poolManager is running. + each: gauge: nilIsZero: true - path: - - status - - poolManagerIsRunning + path: [ status, poolManagerIsRunning ] type: Gauge - help: Whether the repositories poolManager is running. - name: repo_pool_manager_running - - each: + + - name: repo_info + help: Information about a repository. + each: + type: Info info: labelsFromPath: - credentialsName: - - spec - - credentialsName - id: - - status - - id - owner: - - spec - - owner - webhookSecretRefKey: - - spec - - webhookSecretRef - - key - webhookSecretRefName: - - spec - - webhookSecretRef - - name + owner: [ spec, owner ] + credentialsName: [ spec, credentialsName ] + webhookSecretRefKey: [ spec, webhookSecretRef, key ] + webhookSecretRefName: [ spec, webhookSecretRef, name ] + id: [ status, id ] + + - name: repo_annotation_paused_info + help: Whether the repo reconciliation is paused. + each: type: Info - help: Information about a repository. - name: repo_info - - each: info: - labelsFromPath: - paused_value: [] path: - metadata - annotations - garm-operator.mercedes-benz.com/paused - type: Info - help: Whether the repo reconciliation is paused. - name: repo_annotation_paused_info - namespace: - - metadata - - namespace - - commonLabels: - crd_type: pool - groupVersionKind: + labelsFromPath: + paused_value: [ ] + + - groupVersionKind: group: garm-operator.mercedes-benz.com - kind: Pool - version: v1alpha1 - labelsFromPath: - name: - - metadata - - name + kind: "Pool" + version: "v1alpha1" metricNamePrefix: garm_operator + commonLabels: + crd_type: "pool" + labelsFromPath: + name: [ metadata, name ] + namespace: [ metadata, namespace ] metrics: - - each: - gauge: - path: - - metadata - - creationTimestamp - type: Gauge + - name: pool_created help: Unix creation timestamp. - name: pool_created - - each: + each: gauge: path: - - status + - metadata - creationTimestamp type: Gauge - help: Unix creation timestamp. - name: pool_min_idle_runner - - each: + + - name: pool_info + help: Information about a pool. + each: + type: Info info: labelsFromPath: - enabled: - - spec - - enabled - githubRunnerGroup: - - spec - - githubRunnerGroup - id: - - status - - id - imageName: - - spec - - imageName - maxRunners: - - spec - - maxRunners - minIdleRunners: - - spec - - minIdleRunners - osArch: - - spec - - osArch - osType: - - spec - - osType - providerName: - - spec - - providerName - runnerBootstrapTimeout: - - spec - - runnerBootstrapTimeout - runnerPrefix: - - spec - - runnerPrefix - scopeKind: - - spec - - githubScopeRef - - kind - scopeName: - - spec - - githubScopeRef - - name - tags: - - spec - - tags + enabled: [spec, enabled] + githubRunnerGroup: [spec, githubRunnerGroup] + scopeKind: [spec, githubScopeRef, kind] + scopeName: [spec, githubScopeRef, name] + imageName: [spec, imageName] + maxRunners: [spec, maxRunners] + minIdleRunners: [spec, minIdleRunners] + osArch: [spec, osArch] + osType: [spec, osType] + providerName: [spec, providerName] + runnerBootstrapTimeout: [spec, runnerBootstrapTimeout] + runnerPrefix: [spec, runnerPrefix] + tags: [spec, tags] + id: [status, id] + longRunningIdleRunners: [status, longRunningIdleRunners] + + - name: pool_annotation_paused_info + help: Whether the pool reconciliation is paused. + each: type: Info - help: Information about a pool. - name: pool_info - - each: info: - labelsFromPath: - paused_value: [] path: - metadata - annotations - garm-operator.mercedes-benz.com/paused - type: Info - help: Whether the pool reconciliation is paused. - name: pool_annotation_paused_info - namespace: - - metadata - - namespace - - commonLabels: - crd_type: image - groupVersionKind: + labelsFromPath: + paused_value: [ ] + + - groupVersionKind: group: garm-operator.mercedes-benz.com - kind: Image - version: v1alpha1 - labelsFromPath: - name: - - metadata - - name + kind: "Image" + version: "v1alpha1" metricNamePrefix: garm_operator + commonLabels: + crd_type: "image" + labelsFromPath: + name: [ metadata, name ] + namespace: [ metadata, namespace ] metrics: - - each: + - name: image_created + help: Unix creation timestamp. + each: gauge: path: - metadata - creationTimestamp type: Gauge - help: Unix creation timestamp. - name: image_created - - each: + + - name: image_info + help: Information about an image. + each: + type: Info info: labelsFromPath: - tag: - - spec - - tag - type: Info - help: Information about an image. - name: image_info - namespace: - - metadata - - namespace + tag: [spec, tag] diff --git a/docs/kube-state-metrics/kube-state-metrics-config.md b/docs/kube-state-metrics/kube-state-metrics-config.md new file mode 100644 index 00000000..38ed7efa --- /dev/null +++ b/docs/kube-state-metrics/kube-state-metrics-config.md @@ -0,0 +1,96 @@ + + +# Kube State Metrics Configuration + +[Here](../../config/kube-state-metrics/configmap.yaml) you will find a sample configuration for a kube-state-metrics agent to expose metrics of `garm-operators` custom resources. +If you are using the official [helm chart](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-state-metrics) you can place the contents of `.data.config.yaml` into your helm-charts `values.yaml` file like so: + +```yaml +extraArgs: + - --custom-resource-state-only=true + +# Enabling support for customResourceState, will create a configMap including your config that will be read from kube-state-metrics +customResourceState: + enabled: true + config: + kind: CustomResourceStateMetrics + spec: + resources: + - groupVersionKind: + group: garm-operator.mercedes-benz.com + kind: "Enterprise" + version: "v1alpha1" + ... +``` +> Note: this is only a fraction of the kube-state-metrics values file! + +# Metrics + +The following metrics are exposed with this kube-state-metrics configuration: + +#### Image +``` +# HELP garm_operator_image_created Unix creation timestamp. +# TYPE garm_operator_image_created gauge + +# HELP garm_operator_image_info Information about an image. +# TYPE garm_operator_image_info info +``` + +#### Repository +``` +# HELP garm_operator_repo_created Unix creation timestamp. +# TYPE garm_operator_repo_created gauge + +# HELP garm_operator_repo_pool_manager_running Whether the repositories poolManager is running. +# TYPE garm_operator_repo_pool_manager_running gauge + +# HELP garm_operator_repo_info Information about a repository. +# TYPE garm_operator_repo_info info + +# HELP garm_operator_repo_annotation_paused_info Whether the repo reconciliation is paused. +# TYPE garm_operator_repo_annotation_paused_info info +``` + +#### Organization +``` +# HELP garm_operator_org_created Unix creation timestamp. +# TYPE garm_operator_org_created gauge + +# HELP garm_operator_org_pool_manager_running Whether the orgs poolManager is running. +# TYPE garm_operator_org_pool_manager_running gauge + +# HELP garm_operator_org_info Information about an enterprise. +# TYPE garm_operator_org_info info + +# HELP garm_operator_org_annotation_paused_info Whether the org reconciliation is paused. +# TYPE garm_operator_org_annotation_paused_info info +``` + +#### Enterprise +``` +# HELP garm_operator_enterprise_created Unix creation timestamp. +# TYPE garm_operator_enterprise_created gauge + +# HELP garm_operator_enterprise_pool_manager_running Whether the enterprises poolManager is running. +# TYPE garm_operator_enterprise_pool_manager_running gauge + +# HELP garm_operator_enterprise_info Information about an enterprise. +# TYPE garm_operator_enterprise_info info + +# HELP garm_operator_enterprise_annotation_paused_info Whether the enterprise reconciliation is paused. +# TYPE garm_operator_enterprise_annotation_paused_info info +``` + +#### Pool +``` +# HELP garm_operator_pool_created Unix creation timestamp. +# TYPE garm_operator_pool_created gauge + +# HELP garm_operator_pool_info Information about a pool. +# TYPE garm_operator_pool_info info + +# HELP garm_operator_pool_annotation_paused_info Whether the pool reconciliation is paused. +# TYPE garm_operator_pool_annotation_paused_info info +``` + From 69d178bd652e4775523beaa848bdfc5e9b784efe Mon Sep 17 00:00:00 2001 From: rafalgalaw <48678421+rafalgalaw@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:38:27 +0100 Subject: [PATCH 3/5] feat: add conditions to status subresource on CRs (#86) Adds []metav1.Condition property to the status subresource of pools, enterprise, org and repo --- api/v1alpha1/enterprise_types.go | 33 +- api/v1alpha1/organization_types.go | 33 +- api/v1alpha1/pool_types.go | 16 +- api/v1alpha1/pool_types_test.go | 4 +- api/v1alpha1/pool_webhook.go | 2 +- api/v1alpha1/repository_types.go | 33 +- api/v1alpha1/shared.go | 5 + api/v1alpha1/zz_generated.deepcopy.go | 37 +- ...perator.mercedes-benz.com_enterprises.yaml | 73 +++- ...rator.mercedes-benz.com_organizations.yaml | 73 +++- ...garm-operator.mercedes-benz.com_pools.yaml | 72 +++- ...erator.mercedes-benz.com_repositories.yaml | 73 +++- config/overlays/local/manager_patch.yaml | 1 + internal/controller/enterprise_controller.go | 36 +- .../controller/enterprise_controller_test.go | 230 ++++++++++++- .../controller/organization_controller.go | 35 +- .../organization_controller_test.go | 230 ++++++++++++- internal/controller/pool_controller.go | 84 ++--- internal/controller/pool_controller_test.go | 317 +++++++++++++++--- internal/controller/repository_controller.go | 36 +- .../controller/repository_controller_test.go | 172 +++++++++- pkg/secret/secret.go | 3 +- pkg/util/conditions/condition_types.go | 40 +++ pkg/util/conditions/conditions.go | 113 +++++++ pkg/util/pool/pool.go | 2 +- 25 files changed, 1581 insertions(+), 172 deletions(-) create mode 100644 pkg/util/conditions/condition_types.go create mode 100644 pkg/util/conditions/conditions.go diff --git a/api/v1alpha1/enterprise_types.go b/api/v1alpha1/enterprise_types.go index 0dfec623..eb77feb3 100644 --- a/api/v1alpha1/enterprise_types.go +++ b/api/v1alpha1/enterprise_types.go @@ -4,6 +4,8 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // EnterpriseSpec defines the desired state of Enterprise @@ -16,9 +18,8 @@ type EnterpriseSpec struct { // EnterpriseStatus defines the observed state of Enterprise type EnterpriseStatus struct { - ID string `json:"id"` - PoolManagerIsRunning bool `json:"poolManagerIsRunning"` - PoolManagerFailureReason string `json:"poolManagerFailureReason,omitempty"` + ID string `json:"id"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } //+kubebuilder:object:root=true @@ -38,6 +39,14 @@ type Enterprise struct { Status EnterpriseStatus `json:"status,omitempty"` } +func (e *Enterprise) SetConditions(conditions []metav1.Condition) { + e.Status.Conditions = conditions +} + +func (e *Enterprise) GetConditions() []metav1.Condition { + return e.Status.Conditions +} + func (e *Enterprise) GetCredentialsName() string { return e.Spec.CredentialsName } @@ -51,11 +60,25 @@ func (e *Enterprise) GetName() string { } func (e *Enterprise) GetPoolManagerIsRunning() bool { - return e.Status.PoolManagerIsRunning + condition := conditions.Get(e, conditions.PoolManager) + if condition == nil { + return false + } + + return condition.Status == TrueAsString } func (e *Enterprise) GetPoolManagerFailureReason() string { - return e.Status.PoolManagerFailureReason + condition := conditions.Get(e, conditions.PoolManager) + if condition == nil { + return "" + } + + if condition.Reason == string(conditions.PoolManagerFailureReason) { + return condition.Message + } + + return "" } func (e *Enterprise) GetKind() string { diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go index 474186a9..a15d976f 100644 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -4,6 +4,8 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // OrganizationSpec defines the desired state of Organization @@ -16,9 +18,8 @@ type OrganizationSpec struct { // OrganizationStatus defines the observed state of Organization type OrganizationStatus struct { - ID string `json:"id"` - PoolManagerIsRunning bool `json:"poolManagerIsRunning"` - PoolManagerFailureReason string `json:"poolManagerFailureReason,omitempty"` + ID string `json:"id"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } //+kubebuilder:object:root=true @@ -38,6 +39,14 @@ type Organization struct { Status OrganizationStatus `json:"status,omitempty"` } +func (o *Organization) SetConditions(conditions []metav1.Condition) { + o.Status.Conditions = conditions +} + +func (o *Organization) GetConditions() []metav1.Condition { + return o.Status.Conditions +} + func (o *Organization) GetCredentialsName() string { return o.Spec.CredentialsName } @@ -51,11 +60,25 @@ func (o *Organization) GetName() string { } func (o *Organization) GetPoolManagerIsRunning() bool { - return o.Status.PoolManagerIsRunning + condition := conditions.Get(o, conditions.PoolManager) + if condition == nil { + return false + } + + return condition.Status == TrueAsString } func (o *Organization) GetPoolManagerFailureReason() string { - return o.Status.PoolManagerFailureReason + condition := conditions.Get(o, conditions.PoolManager) + if condition == nil { + return "" + } + + if condition.Reason == string(conditions.PoolManagerFailureReason) { + return condition.Message + } + + return "" } func (o *Organization) GetKind() string { diff --git a/api/v1alpha1/pool_types.go b/api/v1alpha1/pool_types.go index 714b839b..dcad1484 100644 --- a/api/v1alpha1/pool_types.go +++ b/api/v1alpha1/pool_types.go @@ -49,7 +49,15 @@ type PoolStatus struct { LongRunningIdleRunners uint `json:"longRunningIdleRunners"` Selector string `json:"selector"` - LastSyncError string `json:"lastSyncError,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func (p *Pool) SetConditions(conditions []metav1.Condition) { + p.Status.Conditions = conditions +} + +func (p *Pool) GetConditions() []metav1.Condition { + return p.Status.Conditions } //+kubebuilder:object:root=true @@ -60,7 +68,7 @@ type PoolStatus struct { //+kubebuilder:printcolumn:name="MinIdleRunners",type=string,JSONPath=`.spec.minIdleRunners` //+kubebuilder:printcolumn:name="MaxRunners",type=string,JSONPath=`.spec.maxRunners` //+kubebuilder:printcolumn:name="ImageName",type=string,JSONPath=`.spec.imageName`,priority=1 -//+kubebuilder:printcolumn:name="Flavour",type=string,JSONPath=`.spec.flavor`,priority=1 +//+kubebuilder:printcolumn:name="Flavor",type=string,JSONPath=`.spec.flavor`,priority=1 //+kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.providerName`,priority=1 //+kubebuilder:printcolumn:name="ScopeType",type=string,JSONPath=`.spec.githubScopeRef.kind`,priority=1 //+kubebuilder:printcolumn:name="ScopeName",type=string,JSONPath=`.spec.githubScopeRef.name`,priority=1 @@ -99,9 +107,9 @@ func MatchesImage(image string) Predicate { } } -func MatchesFlavour(flavour string) Predicate { +func MatchesFlavor(flavor string) Predicate { return func(p Pool) bool { - return p.Spec.Flavor == flavour + return p.Spec.Flavor == flavor } } diff --git a/api/v1alpha1/pool_types_test.go b/api/v1alpha1/pool_types_test.go index 43addaab..4bb932a9 100644 --- a/api/v1alpha1/pool_types_test.go +++ b/api/v1alpha1/pool_types_test.go @@ -70,7 +70,7 @@ func TestPoolList_FilterByFields(t *testing.T) { args: args{ predicates: []Predicate{ MatchesImage("ubuntu-2204"), - MatchesFlavour("large"), + MatchesFlavor("large"), MatchesProvider("openstack"), MatchesGitHubScope("test", "Enterprise"), }, @@ -122,7 +122,7 @@ func TestPoolList_FilterByFields(t *testing.T) { args: args{ predicates: []Predicate{ MatchesImage("ubuntu-2404"), - MatchesFlavour("large"), + MatchesFlavor("large"), MatchesProvider("openstack"), MatchesGitHubScope("test", "Enterprise"), }, diff --git a/api/v1alpha1/pool_webhook.go b/api/v1alpha1/pool_webhook.go index 08106eb7..e9ea353c 100644 --- a/api/v1alpha1/pool_webhook.go +++ b/api/v1alpha1/pool_webhook.go @@ -59,7 +59,7 @@ func (r *Pool) ValidateCreate() (admission.Warnings, error) { } poolList.FilterByFields( - MatchesFlavour(r.Spec.Flavor), + MatchesFlavor(r.Spec.Flavor), MatchesImage(r.Spec.ImageName), MatchesProvider(r.Spec.ProviderName), MatchesGitHubScope(r.Spec.GitHubScopeRef.Name, r.Spec.GitHubScopeRef.Kind), diff --git a/api/v1alpha1/repository_types.go b/api/v1alpha1/repository_types.go index b7d9609a..c47aec36 100644 --- a/api/v1alpha1/repository_types.go +++ b/api/v1alpha1/repository_types.go @@ -4,6 +4,8 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // RepositorySpec defines the desired state of Repository @@ -17,9 +19,8 @@ type RepositorySpec struct { // RepositoryStatus defines the observed state of Repository type RepositoryStatus struct { - ID string `json:"id"` - PoolManagerIsRunning bool `json:"poolManagerIsRunning"` - PoolManagerFailureReason string `json:"poolManagerFailureReason,omitempty"` + ID string `json:"id"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } //+kubebuilder:object:root=true @@ -39,6 +40,14 @@ type Repository struct { Status RepositoryStatus `json:"status,omitempty"` } +func (r *Repository) SetConditions(conditions []metav1.Condition) { + r.Status.Conditions = conditions +} + +func (r *Repository) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + func (r *Repository) GetCredentialsName() string { return r.Spec.CredentialsName } @@ -52,11 +61,25 @@ func (r *Repository) GetName() string { } func (r *Repository) GetPoolManagerIsRunning() bool { - return r.Status.PoolManagerIsRunning + condition := conditions.Get(r, conditions.PoolManager) + if condition == nil { + return false + } + + return condition.Status == TrueAsString } func (r *Repository) GetPoolManagerFailureReason() string { - return r.Status.PoolManagerFailureReason + condition := conditions.Get(r, conditions.PoolManager) + if condition == nil { + return "" + } + + if condition.Reason == string(conditions.PoolManagerFailureReason) { + return condition.Message + } + + return "" } func (r *Repository) GetKind() string { diff --git a/api/v1alpha1/shared.go b/api/v1alpha1/shared.go index 253332ab..b7f270ae 100644 --- a/api/v1alpha1/shared.go +++ b/api/v1alpha1/shared.go @@ -39,3 +39,8 @@ type SecretRef struct { // Key is the key in the secret's data map for this value Key string `json:"key"` } + +const ( + TrueAsString = "True" + FalseAsString = "False" +) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a07e1bc1..1ad46040 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -9,6 +9,7 @@ package v1alpha1 import ( "github.com/cloudbase/garm-provider-common/params" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -18,7 +19,7 @@ func (in *Enterprise) DeepCopyInto(out *Enterprise) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Enterprise. @@ -90,6 +91,13 @@ func (in *EnterpriseSpec) DeepCopy() *EnterpriseSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnterpriseStatus) DeepCopyInto(out *EnterpriseStatus) { *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]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnterpriseStatus. @@ -197,7 +205,7 @@ func (in *Organization) DeepCopyInto(out *Organization) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Organization. @@ -269,6 +277,13 @@ func (in *OrganizationSpec) DeepCopy() *OrganizationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OrganizationStatus) DeepCopyInto(out *OrganizationStatus) { *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]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationStatus. @@ -287,7 +302,7 @@ func (in *Pool) DeepCopyInto(out *Pool) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pool. @@ -364,6 +379,13 @@ func (in *PoolSpec) DeepCopy() *PoolSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PoolStatus) DeepCopyInto(out *PoolStatus) { *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]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PoolStatus. @@ -382,7 +404,7 @@ func (in *Repository) DeepCopyInto(out *Repository) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Repository. @@ -454,6 +476,13 @@ func (in *RepositorySpec) DeepCopy() *RepositorySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositoryStatus) DeepCopyInto(out *RepositoryStatus) { *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]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryStatus. diff --git a/config/crd/bases/garm-operator.mercedes-benz.com_enterprises.yaml b/config/crd/bases/garm-operator.mercedes-benz.com_enterprises.yaml index b03d2e72..4860318e 100644 --- a/config/crd/bases/garm-operator.mercedes-benz.com_enterprises.yaml +++ b/config/crd/bases/garm-operator.mercedes-benz.com_enterprises.yaml @@ -80,15 +80,78 @@ spec: status: description: EnterpriseStatus defines the observed state of Enterprise properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array id: type: string - poolManagerFailureReason: - type: string - poolManagerIsRunning: - type: boolean required: - id - - poolManagerIsRunning type: object type: object served: true diff --git a/config/crd/bases/garm-operator.mercedes-benz.com_organizations.yaml b/config/crd/bases/garm-operator.mercedes-benz.com_organizations.yaml index ba767c29..e2c2cdf4 100644 --- a/config/crd/bases/garm-operator.mercedes-benz.com_organizations.yaml +++ b/config/crd/bases/garm-operator.mercedes-benz.com_organizations.yaml @@ -80,15 +80,78 @@ spec: status: description: OrganizationStatus defines the observed state of Organization properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array id: type: string - poolManagerFailureReason: - type: string - poolManagerIsRunning: - type: boolean required: - id - - poolManagerIsRunning type: object type: object served: true diff --git a/config/crd/bases/garm-operator.mercedes-benz.com_pools.yaml b/config/crd/bases/garm-operator.mercedes-benz.com_pools.yaml index c276b0fd..68acc0de 100644 --- a/config/crd/bases/garm-operator.mercedes-benz.com_pools.yaml +++ b/config/crd/bases/garm-operator.mercedes-benz.com_pools.yaml @@ -31,7 +31,7 @@ spec: priority: 1 type: string - jsonPath: .spec.flavor - name: Flavour + name: Flavor priority: 1 type: string - jsonPath: .spec.providerName @@ -147,10 +147,76 @@ spec: status: description: PoolStatus defines the observed state of Pool properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array id: type: string - lastSyncError: - type: string longRunningIdleRunners: type: integer selector: diff --git a/config/crd/bases/garm-operator.mercedes-benz.com_repositories.yaml b/config/crd/bases/garm-operator.mercedes-benz.com_repositories.yaml index e332d9fd..c22f1bc6 100644 --- a/config/crd/bases/garm-operator.mercedes-benz.com_repositories.yaml +++ b/config/crd/bases/garm-operator.mercedes-benz.com_repositories.yaml @@ -83,15 +83,78 @@ spec: status: description: RepositoryStatus defines the observed state of Repository properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array id: type: string - poolManagerFailureReason: - type: string - poolManagerIsRunning: - type: boolean required: - id - - poolManagerIsRunning type: object type: object served: true diff --git a/config/overlays/local/manager_patch.yaml b/config/overlays/local/manager_patch.yaml index 1560b792..180fd2b9 100644 --- a/config/overlays/local/manager_patch.yaml +++ b/config/overlays/local/manager_patch.yaml @@ -14,3 +14,4 @@ spec: - --garm-password=LmrBG1KcBOsDfNKq4cQTGpc0hJ0kejkk - --operator-watch-namespace=garm-operator-system - --operator-min-idle-runners-age=1m + - --operator-runner-reconciliation=true diff --git a/internal/controller/enterprise_controller.go b/internal/controller/enterprise_controller.go index 8bda6b08..75f243d4 100644 --- a/internal/controller/enterprise_controller.go +++ b/internal/controller/enterprise_controller.go @@ -25,6 +25,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/event" "github.com/mercedes-benz/garm-operator/pkg/secret" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // EnterpriseReconciler reconciles a Enterprise object @@ -80,11 +81,22 @@ func (r *EnterpriseReconciler) reconcileNormal(ctx context.Context, client garmC webhookSecret, err := secret.FetchRef(ctx, r.Client, &enterprise.Spec.WebhookSecretRef, enterprise.Namespace) if err != nil { + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + conditions.MarkFalse(enterprise, conditions.SecretReference, conditions.FetchingSecretRefFailedReason, err.Error()) + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } + conditions.MarkTrue(enterprise, conditions.SecretReference, conditions.FetchingSecretRefSuccessReason, "") garmEnterprise, err := r.getExistingGarmEnterprise(ctx, client, enterprise) if err != nil { + event.Error(r.Recorder, enterprise, err.Error()) + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } @@ -93,6 +105,10 @@ func (r *EnterpriseReconciler) reconcileNormal(ctx context.Context, client garmC garmEnterprise, err = r.createEnterprise(ctx, client, enterprise, webhookSecret) if err != nil { event.Error(r.Recorder, enterprise, err.Error()) + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } } @@ -101,14 +117,21 @@ func (r *EnterpriseReconciler) reconcileNormal(ctx context.Context, client garmC garmEnterprise, err = r.updateEnterprise(ctx, client, garmEnterprise.ID, webhookSecret, enterprise.Spec.CredentialsName) if err != nil { event.Error(r.Recorder, enterprise, err.Error()) + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } // set and update enterprise status enterprise.Status.ID = garmEnterprise.ID - enterprise.Status.PoolManagerFailureReason = garmEnterprise.PoolManagerStatus.FailureReason - enterprise.Status.PoolManagerIsRunning = garmEnterprise.PoolManagerStatus.IsRunning + conditions.MarkTrue(enterprise, conditions.PoolManager, conditions.PoolManagerRunningReason, garmEnterprise.PoolManagerStatus.FailureReason) + if !garmEnterprise.PoolManagerStatus.IsRunning { + conditions.MarkFalse(enterprise, conditions.PoolManager, conditions.PoolManagerFailureReason, garmEnterprise.PoolManagerStatus.FailureReason) + } + conditions.MarkTrue(enterprise, conditions.ReadyCondition, conditions.SuccessfulReconcileReason, "") if err := r.Status().Update(ctx, enterprise); err != nil { return ctrl.Result{}, err } @@ -119,7 +142,6 @@ func (r *EnterpriseReconciler) reconcileNormal(ctx context.Context, client garmC } log.Info("reconciling enterprise successfully done") - return ctrl.Result{}, nil } @@ -197,6 +219,10 @@ func (r *EnterpriseReconciler) reconcileDelete(ctx context.Context, scope garmCl log.Info("starting enterprise deletion") event.Deleting(r.Recorder, enterprise, "starting enterprise deletion") + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.DeletingReason, "Deleting Enterprise") + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } err := scope.DeleteEnterprise( enterprises.NewDeleteEnterpriseParams(). @@ -205,6 +231,10 @@ func (r *EnterpriseReconciler) reconcileDelete(ctx context.Context, scope garmCl if err != nil { log.V(1).Info(fmt.Sprintf("client.DeleteEnterprise error: %s", err)) event.Error(r.Recorder, enterprise, err.Error()) + conditions.MarkFalse(enterprise, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, enterprise); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } diff --git a/internal/controller/enterprise_controller_test.go b/internal/controller/enterprise_controller_test.go index 615f5530..56c7a413 100644 --- a/internal/controller/enterprise_controller_test.go +++ b/internal/controller/enterprise_controller_test.go @@ -6,6 +6,7 @@ import ( "context" "reflect" "testing" + "time" "github.com/cloudbase/garm/client/enterprises" "github.com/cloudbase/garm/params" @@ -23,6 +24,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/client/key" "github.com/mercedes-benz/garm-operator/pkg/client/mock" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { @@ -30,12 +32,13 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { defer mockCtrl.Finish() tests := []struct { - name string - object runtime.Object - expectGarmRequest func(m *mock.MockEnterpriseClientMockRecorder) - runtimeObjects []runtime.Object - wantErr bool - expectedObject *garmoperatorv1alpha1.Enterprise + name string + object runtime.Object + expectGarmRequest func(m *mock.MockEnterpriseClientMockRecorder) + runtimeObjects []runtime.Object + wantErr bool + expectedObject *garmoperatorv1alpha1.Enterprise + expectLastSyncTimeAnnotation bool }{ { name: "enterprise exist - update", @@ -86,6 +89,29 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.EnterpriseStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockEnterpriseClientMockRecorder) { @@ -111,6 +137,7 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "enterprise exist but spec has changed - update", @@ -161,6 +188,29 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.EnterpriseStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockEnterpriseClientMockRecorder) { @@ -186,6 +236,7 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "enterprise exist but pool status has changed - update", @@ -224,9 +275,30 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -267,6 +339,7 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "enterprise does not exist - create and update", @@ -300,6 +373,29 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.EnterpriseStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -349,6 +445,7 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "enterprise already exist in garm - update", @@ -382,6 +479,29 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.EnterpriseStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-12345", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -417,6 +537,7 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "enterprise does not exist in garm - create update", @@ -467,6 +588,29 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.EnterpriseStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockEnterpriseClientMockRecorder) { @@ -500,6 +644,65 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, + }, + { + name: "secret ref not found condition", + object: &garmoperatorv1alpha1.Enterprise{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-enterprise", + Namespace: "default", + Finalizers: []string{ + key.EnterpriseFinalizerName, + }, + }, + Spec: garmoperatorv1alpha1.EnterpriseSpec{ + CredentialsName: "foobar", + WebhookSecretRef: garmoperatorv1alpha1.SecretRef{ + Name: "my-webhook-secret", + Key: "webhookSecret", + }, + }, + Status: garmoperatorv1alpha1.EnterpriseStatus{}, + }, + runtimeObjects: []runtime.Object{}, + expectedObject: &garmoperatorv1alpha1.Enterprise{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-enterprise", + Namespace: "default", + Finalizers: []string{ + key.EnterpriseFinalizerName, + }, + }, + Spec: garmoperatorv1alpha1.EnterpriseSpec{ + CredentialsName: "foobar", + WebhookSecretRef: garmoperatorv1alpha1.SecretRef{ + Name: "my-webhook-secret", + Key: "webhookSecret", + }, + }, + Status: garmoperatorv1alpha1.EnterpriseStatus{ + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.ReconcileErrorReason), + Status: metav1.ConditionFalse, + Message: "secrets \"my-webhook-secret\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefFailedReason), + Status: metav1.ConditionFalse, + Message: "secrets \"my-webhook-secret\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, + }, + }, + expectGarmRequest: func(m *mock.MockEnterpriseClientMockRecorder) {}, + expectLastSyncTimeAnnotation: false, + wantErr: true, }, } for _, tt := range tests { @@ -534,15 +737,20 @@ func TestEnterpriseReconciler_reconcileNormal(t *testing.T) { } // test last-sync-time - assert.Equal(t, annotations.HasAnnotation(enterprise, key.LastSyncTimeAnnotation), true) + assert.Equal(t, tt.expectLastSyncTimeAnnotation, annotations.HasAnnotation(enterprise, key.LastSyncTimeAnnotation)) // clear out annotations to avoid comparison errors enterprise.ObjectMeta.Annotations = nil // empty resource version to avoid comparison errors enterprise.ObjectMeta.ResourceVersion = "" + + // clear conditions lastTransitionTime to avoid comparison errors + conditions.NilLastTransitionTime(tt.expectedObject) + conditions.NilLastTransitionTime(enterprise) + if !reflect.DeepEqual(enterprise, tt.expectedObject) { - t.Errorf("EnterpriseReconciler.reconcileNormal() got = %#v, want %#v", enterprise, tt.expectedObject) + t.Errorf("EnterpriseReconciler.reconcileNormal() \ngot = %#v\n want %#v", enterprise, tt.expectedObject) } }) } diff --git a/internal/controller/organization_controller.go b/internal/controller/organization_controller.go index 7e423dfb..98ddd851 100644 --- a/internal/controller/organization_controller.go +++ b/internal/controller/organization_controller.go @@ -25,6 +25,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/event" "github.com/mercedes-benz/garm-operator/pkg/secret" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // OrganizationReconciler reconciles a Organization object @@ -79,11 +80,22 @@ func (r *OrganizationReconciler) reconcileNormal(ctx context.Context, client gar webhookSecret, err := secret.FetchRef(ctx, r.Client, &organization.Spec.WebhookSecretRef, organization.Namespace) if err != nil { + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + conditions.MarkFalse(organization, conditions.SecretReference, conditions.FetchingSecretRefFailedReason, err.Error()) + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } + conditions.MarkTrue(organization, conditions.SecretReference, conditions.FetchingSecretRefSuccessReason, "") garmOrganization, err := r.getExistingGarmOrg(ctx, client, organization) if err != nil { + event.Error(r.Recorder, organization, err.Error()) + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } @@ -92,6 +104,10 @@ func (r *OrganizationReconciler) reconcileNormal(ctx context.Context, client gar garmOrganization, err = r.createOrganization(ctx, client, organization, webhookSecret) if err != nil { event.Error(r.Recorder, organization, err.Error()) + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } } @@ -100,14 +116,21 @@ func (r *OrganizationReconciler) reconcileNormal(ctx context.Context, client gar garmOrganization, err = r.updateOrganization(ctx, client, garmOrganization.ID, webhookSecret, organization.Spec.CredentialsName) if err != nil { event.Error(r.Recorder, organization, err.Error()) + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } // set and update organization status organization.Status.ID = garmOrganization.ID - organization.Status.PoolManagerFailureReason = garmOrganization.PoolManagerStatus.FailureReason - organization.Status.PoolManagerIsRunning = garmOrganization.PoolManagerStatus.IsRunning + conditions.MarkTrue(organization, conditions.PoolManager, conditions.PoolManagerRunningReason, garmOrganization.PoolManagerStatus.FailureReason) + if !garmOrganization.PoolManagerStatus.IsRunning { + conditions.MarkFalse(organization, conditions.PoolManager, conditions.PoolManagerFailureReason, garmOrganization.PoolManagerStatus.FailureReason) + } + conditions.MarkTrue(organization, conditions.ReadyCondition, conditions.SuccessfulReconcileReason, "") if err := r.Status().Update(ctx, organization); err != nil { return ctrl.Result{}, err } @@ -196,6 +219,10 @@ func (r *OrganizationReconciler) reconcileDelete(ctx context.Context, scope garm log.Info("starting organization deletion") event.Deleting(r.Recorder, organization, "starting organization deletion") + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.DeletingReason, "Deleting Org") + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } err := scope.DeleteOrganization( organizations.NewDeleteOrgParams(). @@ -204,6 +231,10 @@ func (r *OrganizationReconciler) reconcileDelete(ctx context.Context, scope garm if err != nil { log.V(1).Info(fmt.Sprintf("client.DeleteOrganization error: %s", err)) event.Error(r.Recorder, organization, err.Error()) + conditions.MarkFalse(organization, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, organization); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } diff --git a/internal/controller/organization_controller_test.go b/internal/controller/organization_controller_test.go index b46e3b31..3926d108 100644 --- a/internal/controller/organization_controller_test.go +++ b/internal/controller/organization_controller_test.go @@ -6,6 +6,7 @@ import ( "context" "reflect" "testing" + "time" "github.com/cloudbase/garm/client/organizations" "github.com/cloudbase/garm/params" @@ -23,6 +24,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/client/key" "github.com/mercedes-benz/garm-operator/pkg/client/mock" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) func TestOrganizationReconciler_reconcileNormal(t *testing.T) { @@ -30,12 +32,13 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { defer mockCtrl.Finish() tests := []struct { - name string - object runtime.Object - runtimeObjects []runtime.Object - expectGarmRequest func(m *mock.MockOrganizationClientMockRecorder) - wantErr bool - expectedObject *garmoperatorv1alpha1.Organization + name string + object runtime.Object + runtimeObjects []runtime.Object + expectGarmRequest func(m *mock.MockOrganizationClientMockRecorder) + wantErr bool + expectedObject *garmoperatorv1alpha1.Organization + expectLastSyncTimeAnnotation bool }{ { name: "organization exist - update", @@ -86,6 +89,29 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.OrganizationStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -111,6 +137,7 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "organization exist but spec has changed - update", @@ -161,6 +188,29 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.OrganizationStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -186,6 +236,7 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "organization exist but pool status has changed - update", @@ -235,9 +286,30 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, Status: garmoperatorv1alpha1.OrganizationStatus{ - ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -267,6 +339,7 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "organization does not exist - create and update", @@ -311,6 +384,29 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.OrganizationStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -349,6 +445,7 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "organization already exist in garm - update", @@ -393,6 +490,29 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.OrganizationStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-12345", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -417,6 +537,7 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "organization does not exist in garm - create and update", @@ -467,6 +588,29 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.OrganizationStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) { @@ -500,6 +644,65 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, + }, + { + name: "secret ref not found condition", + object: &garmoperatorv1alpha1.Organization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-organization", + Namespace: "default", + Finalizers: []string{ + key.OrganizationFinalizerName, + }, + }, + Spec: garmoperatorv1alpha1.OrganizationSpec{ + CredentialsName: "foobar", + WebhookSecretRef: garmoperatorv1alpha1.SecretRef{ + Name: "my-webhook-secret", + Key: "webhookSecret", + }, + }, + Status: garmoperatorv1alpha1.OrganizationStatus{}, + }, + runtimeObjects: []runtime.Object{}, + expectedObject: &garmoperatorv1alpha1.Organization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-organization", + Namespace: "default", + Finalizers: []string{ + key.OrganizationFinalizerName, + }, + }, + Spec: garmoperatorv1alpha1.OrganizationSpec{ + CredentialsName: "foobar", + WebhookSecretRef: garmoperatorv1alpha1.SecretRef{ + Name: "my-webhook-secret", + Key: "webhookSecret", + }, + }, + Status: garmoperatorv1alpha1.OrganizationStatus{ + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.ReconcileErrorReason), + Status: metav1.ConditionFalse, + Message: "secrets \"my-webhook-secret\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefFailedReason), + Status: metav1.ConditionFalse, + Message: "secrets \"my-webhook-secret\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, + }, + }, + expectGarmRequest: func(m *mock.MockOrganizationClientMockRecorder) {}, + expectLastSyncTimeAnnotation: false, + wantErr: true, }, } for _, tt := range tests { @@ -534,15 +737,20 @@ func TestOrganizationReconciler_reconcileNormal(t *testing.T) { } // test last-sync-time - assert.Equal(t, annotations.HasAnnotation(organization, key.LastSyncTimeAnnotation), true) + assert.Equal(t, tt.expectLastSyncTimeAnnotation, annotations.HasAnnotation(organization, key.LastSyncTimeAnnotation)) // clear out annotations to avoid comparison errors organization.ObjectMeta.Annotations = nil // empty resource version to avoid comparison errors organization.ObjectMeta.ResourceVersion = "" + + // clear conditions lastTransitionTime to avoid comparison errors + conditions.NilLastTransitionTime(tt.expectedObject) + conditions.NilLastTransitionTime(organization) + if !reflect.DeepEqual(organization, tt.expectedObject) { - t.Errorf("OrganizationReconciler.reconcileNormal() got = %#v, want %#v", organization, tt.expectedObject) + t.Errorf("OrganizationReconciler.reconcileNormal() \ngot = %#v\n want %#v", organization, tt.expectedObject) } }) } diff --git a/internal/controller/pool_controller.go b/internal/controller/pool_controller.go index 35ce5644..3ca304e9 100644 --- a/internal/controller/pool_controller.go +++ b/internal/controller/pool_controller.go @@ -5,7 +5,6 @@ package controller import ( "context" "encoding/json" - "errors" "fmt" "reflect" "sort" @@ -34,6 +33,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/config" "github.com/mercedes-benz/garm-operator/pkg/event" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" poolUtil "github.com/mercedes-benz/garm-operator/pkg/util/pool" ) @@ -62,7 +62,7 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{}, nil } log.Error(err, "cannot fetch Pool") - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } // Ignore objects that are paused @@ -84,22 +84,21 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } func (r *PoolReconciler) reconcileNormal(ctx context.Context, poolClient garmClient.PoolClient, pool *garmoperatorv1alpha1.Pool, instanceClient garmClient.InstanceClient) (ctrl.Result, error) { - log := log.FromContext(ctx). - WithName("reconcileNormal") - gitHubScopeRef, err := r.fetchGitHubScopeCRD(ctx, pool) if err != nil { - msg := fmt.Sprintf("Error: %s, referenced GitHubScopeRef %s/%s not found", err.Error(), pool.Spec.GitHubScopeRef.Kind, pool.Spec.GitHubScopeRef.Name) - log.Error(err, msg) - return r.handleUpdateError(ctx, pool, errors.New(msg)) + conditions.MarkFalse(pool, conditions.ScopeReference, conditions.FetchingScopeRefFailedReason, err.Error()) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } if gitHubScopeRef.GetID() == "" { - return r.handleUpdateError(ctx, pool, fmt.Errorf("referenced GitHubScopeRef %s/%s not ready yet", pool.Spec.GitHubScopeRef.Kind, pool.Spec.GitHubScopeRef.Name)) + err := fmt.Errorf("referenced GitHubScopeRef %s/%s not ready yet", pool.Spec.GitHubScopeRef.Kind, pool.Spec.GitHubScopeRef.Name) + conditions.MarkFalse(pool, conditions.ScopeReference, conditions.ScopeRefNotReadyReason, err.Error()) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } + conditions.MarkTrue(pool, conditions.ScopeReference, conditions.FetchingScopeRefSuccessReason, fmt.Sprintf("Successfully fetched %s CR Ref", pool.Spec.GitHubScopeRef.Kind)) if err := r.ensureFinalizer(ctx, pool); err != nil { - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } // pool status id has been set and id matches existing garm pool, so we know that the pool is persisted in garm and needs to be updated @@ -118,29 +117,38 @@ func (r *PoolReconciler) reconcileCreate(ctx context.Context, garmClient garmCli // get image cr object by name image, err := r.getImage(ctx, pool) if err != nil { - return r.handleUpdateError(ctx, pool, err) + conditions.MarkFalse(pool, conditions.ImageReference, conditions.FetchingImageRefFailedReason, err.Error()) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } + conditions.MarkTrue(pool, conditions.ImageReference, conditions.FetchingImageRefSuccessReason, "Successfully fetched Image CR Ref") // check if there is already a pool with the same spec on garm side matchingGarmPool, err := poolUtil.GetGarmPoolBySpecs(ctx, garmClient, pool, image, gitHubScopeRef) if err != nil { - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } if matchingGarmPool != nil { log.Info("Found garm pool with matching specs, syncing IDs", "garmID", matchingGarmPool.ID) event.Creating(r.Recorder, pool, fmt.Sprintf("found garm pool with matching specs, syncing IDs: %s", matchingGarmPool.ID)) - return r.handleSuccessfulUpdate(ctx, pool, *matchingGarmPool) + + pool.Status.ID = matchingGarmPool.ID + pool.Status.LongRunningIdleRunners = matchingGarmPool.MinIdleRunners + return r.handleSuccessfulUpdate(ctx, pool) } // create new pool in garm garmPool, err := poolUtil.CreatePool(ctx, garmClient, pool, image, gitHubScopeRef) if err != nil { - return r.handleUpdateError(ctx, pool, fmt.Errorf("failed creating pool %s: %s", pool.Name, err.Error())) + return r.handleUpdateError(ctx, pool, fmt.Errorf("failed creating pool %s: %s", pool.Name, err.Error()), conditions.ReconcileErrorReason) } + log.Info("creating pool in garm succeeded") event.Info(r.Recorder, pool, "creating pool in garm succeeded") - return r.handleSuccessfulUpdate(ctx, pool, garmPool) + + pool.Status.ID = garmPool.ID + pool.Status.LongRunningIdleRunners = garmPool.MinIdleRunners + return r.handleSuccessfulUpdate(ctx, pool) } func (r *PoolReconciler) reconcileUpdate(ctx context.Context, garmClient garmClient.PoolClient, pool *garmoperatorv1alpha1.Pool, instanceClient garmClient.InstanceClient) (ctrl.Result, error) { @@ -150,13 +158,15 @@ func (r *PoolReconciler) reconcileUpdate(ctx context.Context, garmClient garmCli image, err := r.getImage(ctx, pool) if err != nil { - return r.handleUpdateError(ctx, pool, err) + conditions.MarkFalse(pool, conditions.ImageReference, conditions.FetchingImageRefFailedReason, err.Error()) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } + conditions.MarkTrue(pool, conditions.ImageReference, conditions.FetchingImageRefSuccessReason, "Successfully fetched Image CR Ref") poolCRdiffersFromGarmPool, idleRunners, err := r.comparePoolSpecs(ctx, pool, image.Spec.Tag, garmClient) if err != nil { log.Error(err, "error comparing pool specs") - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } if !poolCRdiffersFromGarmPool { @@ -164,7 +174,7 @@ func (r *PoolReconciler) reconcileUpdate(ctx context.Context, garmClient garmCli if err = poolUtil.UpdatePool(ctx, garmClient, pool, image); err != nil { log.Error(err, "error updating pool") - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } } @@ -211,18 +221,9 @@ func (r *PoolReconciler) reconcileUpdate(ctx context.Context, garmClient garmCli // update pool idle runners count in status if pool.Status.LongRunningIdleRunners != uint(longRunningIdleRunnersCount) { pool.Status.LongRunningIdleRunners = uint(longRunningIdleRunnersCount) - if err := r.updatePoolCRStatus(ctx, pool); err != nil { - return ctrl.Result{}, err - } - } - - err = annotations.SetLastSyncTime(pool, r.Client) - if err != nil { - log.Error(err, "can not set annotation") - return ctrl.Result{}, err } - return ctrl.Result{}, nil + return r.handleSuccessfulUpdate(ctx, pool) } func (r *PoolReconciler) reconcileDelete(ctx context.Context, garmClient garmClient.PoolClient, pool *garmoperatorv1alpha1.Pool, instanceClient garmClient.InstanceClient) (ctrl.Result, error) { @@ -230,12 +231,16 @@ func (r *PoolReconciler) reconcileDelete(ctx context.Context, garmClient garmCli log := log.FromContext(ctx) log.Info("Deleting Pool", "pool", pool.Name) event.Deleting(r.Recorder, pool, "") + conditions.MarkFalse(pool, conditions.ReadyCondition, conditions.DeletingReason, "Deleting Pool") + if err := r.Status().Update(ctx, pool); err != nil { + return ctrl.Result{}, err + } // this is to make the deletion of a "pending" pool CR possible if pool.Status.ID == "" && controllerutil.ContainsFinalizer(pool, key.PoolFinalizerName) { controllerutil.RemoveFinalizer(pool, key.PoolFinalizerName) if err := r.Update(ctx, pool); err != nil { - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } log.Info("Successfully deleted pool", "pool", pool.Name) @@ -245,12 +250,12 @@ func (r *PoolReconciler) reconcileDelete(ctx context.Context, garmClient garmCli pool.Spec.MinIdleRunners = 0 pool.Spec.Enabled = false if err := r.Update(ctx, pool); err != nil { - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } if err := poolUtil.UpdatePool(ctx, garmClient, pool, nil); err != nil { log.Error(err, "error updating pool") - return r.handleUpdateError(ctx, pool, err) + return r.handleUpdateError(ctx, pool, err, conditions.ReconcileErrorReason) } // get all runners @@ -280,13 +285,13 @@ func (r *PoolReconciler) reconcileDelete(ctx context.Context, garmClient garmCli // delete pool in garm if err = garmClient.DeletePool(pools.NewDeletePoolParams().WithPoolID(pool.Status.ID)); err != nil { - return r.handleUpdateError(ctx, pool, fmt.Errorf("error deleting pool %s: %w", pool.Name, err)) + return r.handleUpdateError(ctx, pool, fmt.Errorf("error deleting pool %s: %w", pool.Name, err), conditions.DeletionFailedReason) } // remove finalizer so k8s can delete resource controllerutil.RemoveFinalizer(pool, key.PoolFinalizerName) if err := r.Update(ctx, pool); err != nil { - return r.handleUpdateError(ctx, pool, fmt.Errorf("error deleting pool %s: %w", pool.Name, err)) + return r.handleUpdateError(ctx, pool, fmt.Errorf("error deleting pool %s: %w", pool.Name, err), conditions.DeletionFailedReason) } log.Info("Successfully deleted pool", "pool", pool.Name) @@ -302,13 +307,16 @@ func (r *PoolReconciler) updatePoolCRStatus(ctx context.Context, pool *garmopera return nil } -func (r *PoolReconciler) handleUpdateError(ctx context.Context, pool *garmoperatorv1alpha1.Pool, err error) (ctrl.Result, error) { +func (r *PoolReconciler) handleUpdateError(ctx context.Context, pool *garmoperatorv1alpha1.Pool, err error, conditionReason conditions.ConditionReason) (ctrl.Result, error) { log := log.FromContext(ctx) log.Error(err, "error") event.Error(r.Recorder, pool, err.Error()) - pool.Status.LastSyncError = err.Error() + conditions.MarkFalse(pool, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if conditionReason != "" { + conditions.MarkFalse(pool, conditions.ReadyCondition, conditionReason, err.Error()) + } if updateErr := r.updatePoolCRStatus(ctx, pool); updateErr != nil { return ctrl.Result{}, updateErr @@ -317,12 +325,10 @@ func (r *PoolReconciler) handleUpdateError(ctx context.Context, pool *garmoperat return ctrl.Result{}, err } -func (r *PoolReconciler) handleSuccessfulUpdate(ctx context.Context, pool *garmoperatorv1alpha1.Pool, garmPool params.Pool) (ctrl.Result, error) { +func (r *PoolReconciler) handleSuccessfulUpdate(ctx context.Context, pool *garmoperatorv1alpha1.Pool) (ctrl.Result, error) { log := log.FromContext(ctx) - pool.Status.ID = garmPool.ID - pool.Status.LongRunningIdleRunners = garmPool.MinIdleRunners - pool.Status.LastSyncError = "" + conditions.MarkTrue(pool, conditions.ReadyCondition, conditions.SuccessfulReconcileReason, "") if err := r.updatePoolCRStatus(ctx, pool); err != nil { return ctrl.Result{}, err diff --git a/internal/controller/pool_controller_test.go b/internal/controller/pool_controller_test.go index cac1b195..097a6b7d 100644 --- a/internal/controller/pool_controller_test.go +++ b/internal/controller/pool_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/client/mock" "github.com/mercedes-benz/garm-operator/pkg/config" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) const namespaceName = "test-namespace" @@ -122,7 +123,22 @@ func TestPoolController_ReconcileCreate(t *testing.T) { ID: poolID, LongRunningIdleRunners: 3, Selector: "", - LastSyncError: "", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.SuccessfulReconcileReason), + Message: "", + }, + { + Type: string(conditions.ImageReference), + Status: metav1.ConditionTrue, + Message: "Successfully fetched Image CR Ref", + Reason: string(conditions.FetchingImageRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -161,9 +177,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -265,8 +295,7 @@ func TestPoolController_ReconcileCreate(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: outdatedPoolID, - LastSyncError: "", + ID: outdatedPoolID, }, }, expectedObject: &garmoperatorv1alpha1.Pool{ @@ -304,7 +333,22 @@ func TestPoolController_ReconcileCreate(t *testing.T) { ID: poolID, LongRunningIdleRunners: 3, Selector: "", - LastSyncError: "", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.SuccessfulReconcileReason), + Message: "", + }, + { + Type: string(conditions.ImageReference), + Status: metav1.ConditionTrue, + Message: "Successfully fetched Image CR Ref", + Reason: string(conditions.FetchingImageRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -343,9 +387,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -427,8 +485,8 @@ func TestPoolController_ReconcileCreate(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + // LastSyncError: "", }, }, expectedObject: &garmoperatorv1alpha1.Pool{ @@ -463,8 +521,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.SuccessfulReconcileReason), + Message: "", + }, + { + Type: string(conditions.ImageReference), + Status: metav1.ConditionTrue, + Message: "Successfully fetched Image CR Ref", + Reason: string(conditions.FetchingImageRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -503,9 +576,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -686,7 +773,7 @@ func TestPoolController_ReconcileCreate(t *testing.T) { Status: garmoperatorv1alpha1.PoolStatus{ ID: poolID, LongRunningIdleRunners: 3, - LastSyncError: "", + // LastSyncError: "", }, }, expectLastSyncTimeAnnotation: true, @@ -724,7 +811,22 @@ func TestPoolController_ReconcileCreate(t *testing.T) { Status: garmoperatorv1alpha1.PoolStatus{ ID: poolID, LongRunningIdleRunners: 2, - LastSyncError: "", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.SuccessfulReconcileReason), + Message: "", + }, + { + Type: string(conditions.ImageReference), + Status: metav1.ConditionTrue, + Message: "Successfully fetched Image CR Ref", + Reason: string(conditions.FetchingImageRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -763,9 +865,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -1088,7 +1204,22 @@ func TestPoolController_ReconcileCreate(t *testing.T) { ID: "", LongRunningIdleRunners: 0, Selector: "", - LastSyncError: "images.garm-operator.mercedes-benz.com \"ubuntu-image\" not found", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.ReconcileErrorReason), + Status: metav1.ConditionFalse, + Message: "images.garm-operator.mercedes-benz.com \"ubuntu-image\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.ImageReference), + Reason: string(conditions.FetchingImageRefFailedReason), + Status: metav1.ConditionFalse, + Message: "images.garm-operator.mercedes-benz.com \"ubuntu-image\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -1118,9 +1249,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -1160,8 +1305,8 @@ func TestPoolController_ReconcileCreate(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + // LastSyncError: "", }, }, expectedObject: &garmoperatorv1alpha1.Pool{ @@ -1196,8 +1341,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "images.garm-operator.mercedes-benz.com \"ubuntu-image-not-existent\" not found", + ID: poolID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.ReconcileErrorReason), + Status: metav1.ConditionFalse, + Message: "images.garm-operator.mercedes-benz.com \"ubuntu-image-not-existent\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.ImageReference), + Reason: string(conditions.FetchingImageRefFailedReason), + Status: metav1.ConditionFalse, + Message: "images.garm-operator.mercedes-benz.com \"ubuntu-image-not-existent\" not found", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -1236,9 +1396,23 @@ func TestPoolController_ReconcileCreate(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -1334,6 +1508,11 @@ func TestPoolController_ReconcileCreate(t *testing.T) { // empty resource version to avoid comparison errors pool.ObjectMeta.ResourceVersion = "" + + // clear conditions lastTransitionTime to avoid comparison errors + conditions.NilLastTransitionTime(tt.expectedObject) + conditions.NilLastTransitionTime(pool) + if !reflect.DeepEqual(pool, tt.expectedObject) { t.Errorf("PoolReconciler.reconcileNormal() \n got = %#v \n want = %#v", pool, tt.expectedObject) } @@ -1395,8 +1574,8 @@ func TestPoolController_ReconcileDelete(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + // LastSyncError: "", }, }, expectedObject: &garmoperatorv1alpha1.Pool{ @@ -1429,8 +1608,16 @@ func TestPoolController_ReconcileDelete(t *testing.T) { }, Status: garmoperatorv1alpha1.PoolStatus{ ID: poolID, - LastSyncError: "", LongRunningIdleRunners: 0, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.DeletingReason), + Message: "Deleting Pool", + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -1469,9 +1656,23 @@ func TestPoolController_ReconcileDelete(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -1586,8 +1787,8 @@ func TestPoolController_ReconcileDelete(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + // LastSyncError: "", LongRunningIdleRunners: 0, }, }, @@ -1620,8 +1821,16 @@ func TestPoolController_ReconcileDelete(t *testing.T) { GitHubRunnerGroup: "", }, Status: garmoperatorv1alpha1.PoolStatus{ - ID: poolID, - LastSyncError: "", + ID: poolID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.DeletingReason), + Message: "Deleting Pool", + }, + }, }, }, runtimeObjects: []runtime.Object{ @@ -1660,9 +1869,23 @@ func TestPoolController_ReconcileDelete(t *testing.T) { }, }, Status: garmoperatorv1alpha1.EnterpriseStatus{ - ID: enterpriseID, - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: enterpriseID, + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, }, @@ -1763,6 +1986,8 @@ func TestPoolController_ReconcileDelete(t *testing.T) { // empty resource version to avoid comparison errors pool.ObjectMeta.ResourceVersion = "" pool.Spec.GitHubScopeRef.APIGroup = nil + conditions.NilLastTransitionTime(pool) + conditions.NilLastTransitionTime(tt.expectedObject) if !reflect.DeepEqual(pool, tt.expectedObject) { t.Errorf("PoolReconciler.reconcileNormal() \n got = %#v \n want = %#v", pool, tt.expectedObject) diff --git a/internal/controller/repository_controller.go b/internal/controller/repository_controller.go index 7eb2f24f..91b4b7a3 100644 --- a/internal/controller/repository_controller.go +++ b/internal/controller/repository_controller.go @@ -25,6 +25,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/event" "github.com/mercedes-benz/garm-operator/pkg/secret" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) // RepositoryReconciler reconciles a Repository object @@ -79,11 +80,22 @@ func (r *RepositoryReconciler) reconcileNormal(ctx context.Context, client garmC webhookSecret, err := secret.FetchRef(ctx, r.Client, &repository.Spec.WebhookSecretRef, repository.Namespace) if err != nil { + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + conditions.MarkFalse(repository, conditions.SecretReference, conditions.FetchingSecretRefFailedReason, err.Error()) + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } + conditions.MarkTrue(repository, conditions.SecretReference, conditions.FetchingSecretRefSuccessReason, "") garmRepository, err := r.getExistingGarmRepo(ctx, client, repository) if err != nil { + event.Error(r.Recorder, repository, err.Error()) + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } @@ -92,6 +104,10 @@ func (r *RepositoryReconciler) reconcileNormal(ctx context.Context, client garmC garmRepository, err = r.createRepository(ctx, client, repository, webhookSecret) if err != nil { event.Error(r.Recorder, repository, err.Error()) + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } } @@ -100,13 +116,21 @@ func (r *RepositoryReconciler) reconcileNormal(ctx context.Context, client garmC garmRepository, err = r.updateRepository(ctx, client, garmRepository.ID, webhookSecret, repository.Spec.CredentialsName) if err != nil { event.Error(r.Recorder, repository, err.Error()) + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } + // set and update repository status repository.Status.ID = garmRepository.ID - repository.Status.PoolManagerFailureReason = garmRepository.PoolManagerStatus.FailureReason - repository.Status.PoolManagerIsRunning = garmRepository.PoolManagerStatus.IsRunning + conditions.MarkTrue(repository, conditions.PoolManager, conditions.PoolManagerRunningReason, garmRepository.PoolManagerStatus.FailureReason) + if !garmRepository.PoolManagerStatus.IsRunning { + conditions.MarkFalse(repository, conditions.PoolManager, conditions.PoolManagerFailureReason, garmRepository.PoolManagerStatus.FailureReason) + } + conditions.MarkTrue(repository, conditions.ReadyCondition, conditions.SuccessfulReconcileReason, "") if err := r.Status().Update(ctx, repository); err != nil { return ctrl.Result{}, err } @@ -196,6 +220,10 @@ func (r *RepositoryReconciler) reconcileDelete(ctx context.Context, scope garmCl log.Info("starting repository deletion") event.Deleting(r.Recorder, repository, "starting repository deletion") + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.DeletingReason, "Deleting Repo") + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } err := scope.DeleteRepository( repositories.NewDeleteRepoParams(). @@ -204,6 +232,10 @@ func (r *RepositoryReconciler) reconcileDelete(ctx context.Context, scope garmCl if err != nil { log.V(1).Info(fmt.Sprintf("client.DeleteRepository error: %s", err)) event.Error(r.Recorder, repository, err.Error()) + conditions.MarkFalse(repository, conditions.ReadyCondition, conditions.ReconcileErrorReason, err.Error()) + if err := r.Status().Update(ctx, repository); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } diff --git a/internal/controller/repository_controller_test.go b/internal/controller/repository_controller_test.go index fddd9052..84d9ca53 100644 --- a/internal/controller/repository_controller_test.go +++ b/internal/controller/repository_controller_test.go @@ -6,6 +6,7 @@ import ( "context" "reflect" "testing" + "time" "github.com/cloudbase/garm/client/repositories" "github.com/cloudbase/garm/params" @@ -23,6 +24,7 @@ import ( "github.com/mercedes-benz/garm-operator/pkg/client/key" "github.com/mercedes-benz/garm-operator/pkg/client/mock" "github.com/mercedes-benz/garm-operator/pkg/util/annotations" + "github.com/mercedes-benz/garm-operator/pkg/util/conditions" ) func TestRepositoryReconciler_reconcileNormal(t *testing.T) { @@ -30,12 +32,13 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { defer mockCtrl.Finish() tests := []struct { - name string - object runtime.Object - runtimeObjects []runtime.Object - expectGarmRequest func(m *mock.MockRepositoryClientMockRecorder) - wantErr bool - expectedObject *garmoperatorv1alpha1.Repository + name string + object runtime.Object + runtimeObjects []runtime.Object + expectGarmRequest func(m *mock.MockRepositoryClientMockRecorder) + wantErr bool + expectedObject *garmoperatorv1alpha1.Repository + expectLastSyncTimeAnnotation bool }{ { name: "repository exist - update", @@ -88,6 +91,29 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.RepositoryStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -115,6 +141,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "repository exist but spec has changed - update", @@ -167,6 +194,29 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.RepositoryStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -194,6 +244,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "repository exist but pool status has changed - update", @@ -245,9 +296,30 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, Status: garmoperatorv1alpha1.RepositoryStatus{ - ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", - PoolManagerIsRunning: false, - PoolManagerFailureReason: "no resources available", + ID: "e1dbf9a6-a9f6-4594-a5ac-ae78a8f27a3e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "no resources available", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -279,6 +351,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "repository does not exist - create and update", @@ -325,6 +398,29 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.RepositoryStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -367,6 +463,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "repository already exist in garm - update", @@ -413,6 +510,29 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.RepositoryStatus{ ID: "e1dbf9a6-a9f6-4594-a5ac-12345", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Reason: string(conditions.SuccessfulReconcileReason), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Reason: string(conditions.PoolManagerFailureReason), + Status: metav1.ConditionFalse, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Reason: string(conditions.FetchingSecretRefSuccessReason), + Status: metav1.ConditionTrue, + Message: "", + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -439,6 +559,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, { name: "repository does not exist in garm - create and update", @@ -491,6 +612,29 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, Status: garmoperatorv1alpha1.RepositoryStatus{ ID: "9e0da3cb-130b-428d-aa8a-e314d955060e", + Conditions: []metav1.Condition{ + { + Type: string(conditions.ReadyCondition), + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.NewTime(time.Now()), + Reason: string(conditions.SuccessfulReconcileReason), + Message: "", + }, + { + Type: string(conditions.PoolManager), + Status: metav1.ConditionFalse, + Message: "", + Reason: string(conditions.PoolManagerFailureReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + { + Type: string(conditions.SecretReference), + Status: metav1.ConditionTrue, + Message: "", + Reason: string(conditions.FetchingSecretRefSuccessReason), + LastTransitionTime: metav1.NewTime(time.Now()), + }, + }, }, }, expectGarmRequest: func(m *mock.MockRepositoryClientMockRecorder) { @@ -527,6 +671,7 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { }, }, nil) }, + expectLastSyncTimeAnnotation: true, }, } for _, tt := range tests { @@ -561,15 +706,20 @@ func TestRepositoryReconciler_reconcileNormal(t *testing.T) { } // test last-sync-time - assert.Equal(t, annotations.HasAnnotation(repository, key.LastSyncTimeAnnotation), true) + assert.Equal(t, tt.expectLastSyncTimeAnnotation, annotations.HasAnnotation(repository, key.LastSyncTimeAnnotation)) // clear out annotations to avoid comparison errors repository.ObjectMeta.Annotations = nil // empty resource version to avoid comparison errors repository.ObjectMeta.ResourceVersion = "" + + // clear conditions lastTransitionTime to avoid comparison errors + conditions.NilLastTransitionTime(tt.expectedObject) + conditions.NilLastTransitionTime(repository) + if !reflect.DeepEqual(repository, tt.expectedObject) { - t.Errorf("RepositoryReconciler.reconcileNormal() got = %#v, want %#v", repository, tt.expectedObject) + t.Errorf("RepositoryReconciler.reconcileNormal() \ngot = %#v\n want %#v", repository, tt.expectedObject) } }) } diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index ec1437e5..0c2b57ec 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -6,7 +6,6 @@ import ( "context" "fmt" - "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" @@ -30,7 +29,7 @@ func FetchRef(ctx context.Context, c client.Client, ref *garmoperatorv1alpha1.Se secret) if err != nil { if apierrors.IsNotFound(err) { - return "", errors.Wrapf(err, "secret not found: %s", ref.Name) + return "", err } return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.Name, err) } diff --git a/pkg/util/conditions/condition_types.go b/pkg/util/conditions/condition_types.go new file mode 100644 index 00000000..963bd376 --- /dev/null +++ b/pkg/util/conditions/condition_types.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +package conditions + +type ( + ConditionType string + ConditionReason string +) + +// Common Conditions & Reasons +const ( + ReadyCondition ConditionType = "Ready" + SuccessfulReconcileReason ConditionReason = "ReconcileSuccess" + ReconcileErrorReason ConditionReason = "ReconcileError" + DeletingReason ConditionReason = "Deleting" + DeletionFailedReason ConditionReason = "DeletionFailed" +) + +// Pool Conditions & Reasons +const ( + ImageReference ConditionType = "ImageReference" + FetchingImageRefSuccessReason ConditionReason = "FetchingImageRefSuccess" + FetchingImageRefFailedReason ConditionReason = "FetchingImageRefFailed" + + ScopeReference ConditionType = "ScopeReference" + FetchingScopeRefSuccessReason ConditionReason = "FetchingScopeRefSuccess" + FetchingScopeRefFailedReason ConditionReason = "FetchingScopeRefFailed" + ScopeRefNotReadyReason ConditionReason = "ScopeRefNotReady" +) + +// Enterprise, Org & Repo Conditions +const ( + PoolManager ConditionType = "PoolManager" + PoolManagerRunningReason ConditionReason = "PoolManagerRunning" + PoolManagerFailureReason ConditionReason = "PoolManagerFailure" + + SecretReference ConditionType = "SecretReference" + FetchingSecretRefSuccessReason ConditionReason = "FetchingSecretRefSuccess" + FetchingSecretRefFailedReason ConditionReason = "FetchingSecretRefFailed" +) diff --git a/pkg/util/conditions/conditions.go b/pkg/util/conditions/conditions.go new file mode 100644 index 00000000..39034f3f --- /dev/null +++ b/pkg/util/conditions/conditions.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT + +package conditions + +import ( + "sort" + "time" + + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ConditionStatusObject interface { + SetConditions(conditions []metav1.Condition) + GetConditions() []metav1.Condition +} + +func Get(o ConditionStatusObject, t ConditionType) *metav1.Condition { + if o == nil { + return nil + } + + conditions := o.GetConditions() + if conditions == nil { + return nil + } + + return apimeta.FindStatusCondition(conditions, string(t)) +} + +func Set(o ConditionStatusObject, condition metav1.Condition) { + if o == nil { + return + } + + conditions := o.GetConditions() + apimeta.SetStatusCondition(&conditions, condition) + sort.Slice(conditions, func(i, j int) bool { + return lexicographicLess(&conditions[i], &conditions[j]) + }) + o.SetConditions(conditions) +} + +func Has(o ConditionStatusObject, t ConditionType) bool { + return Get(o, t) != nil +} + +func Remove(o ConditionStatusObject, t ConditionType) { + if o == nil { + return + } + + conditions := o.GetConditions() + apimeta.RemoveStatusCondition(&conditions, string(t)) + o.SetConditions(conditions) +} + +func TrueCondition(t ConditionType, reason ConditionReason, message string) *metav1.Condition { + return &metav1.Condition{ + Type: string(t), + Status: metav1.ConditionTrue, + Reason: string(reason), + Message: message, + } +} + +func FalseCondition(t ConditionType, reason ConditionReason, message string) *metav1.Condition { + return &metav1.Condition{ + Type: string(t), + Status: metav1.ConditionFalse, + Reason: string(reason), + Message: message, + } +} + +func UnknownCondition(t ConditionType, reason ConditionReason, message string) *metav1.Condition { + return &metav1.Condition{ + Type: string(t), + Status: metav1.ConditionUnknown, + Reason: string(reason), + Message: message, + } +} + +func MarkTrue(o ConditionStatusObject, t ConditionType, reason ConditionReason, message string) { + Set(o, *TrueCondition(t, reason, message)) +} + +func MarkFalse(o ConditionStatusObject, t ConditionType, reason ConditionReason, message string) { + Set(o, *FalseCondition(t, reason, message)) +} + +func MarkUnknown(o ConditionStatusObject, t ConditionType, reason ConditionReason, message string) { + Set(o, *UnknownCondition(t, reason, message)) +} + +// lexicographicLess returns true if a condition is less than another with regard to the +// to order of conditions designed for convenience of the consumer, i.e. kubectl. +// According to this order the Ready condition always goes first, followed by all the other +// conditions sorted by Type. +func lexicographicLess(i, j *metav1.Condition) bool { + return (i.Type == string(ReadyCondition) || i.Type < j.Type) && j.Type != string(ReadyCondition) +} + +func NilLastTransitionTime(o ConditionStatusObject) { + conditions := o.GetConditions() + for i := range conditions { + time := metav1.NewTime(time.Now()) + time.Reset() + conditions[i].LastTransitionTime = time + } + o.SetConditions(conditions) +} diff --git a/pkg/util/pool/pool.go b/pkg/util/pool/pool.go index 744af3b2..cf3c2be5 100644 --- a/pkg/util/pool/pool.go +++ b/pkg/util/pool/pool.go @@ -55,7 +55,7 @@ func GetGarmPoolBySpecs(ctx context.Context, garmClient garmClient.PoolClient, p //nolint TODO: @rafalgalaw - can this happen? // i guess it's blocked by the fact that we can't create a pool with the same spec on garm side if len(filteredGarmPools) > 1 { - return nil, errors.New("can not create pool, multiple instances matching flavour, image and provider found in garm") + return nil, errors.New("can not create pool, multiple instances matching flavor, image and provider found in garm") } // pool with the same specs already exists From bb764a4c38eb391f672c0d975c06e241d1eee763 Mon Sep 17 00:00:00 2001 From: rthalho Date: Wed, 28 Feb 2024 13:59:49 +0100 Subject: [PATCH 4/5] fix: make kube-state-metrics docs better readable --- .../kube-state-metrics-config.md | 86 +++++++------------ 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/docs/kube-state-metrics/kube-state-metrics-config.md b/docs/kube-state-metrics/kube-state-metrics-config.md index 38ed7efa..a643f146 100644 --- a/docs/kube-state-metrics/kube-state-metrics-config.md +++ b/docs/kube-state-metrics/kube-state-metrics-config.md @@ -29,68 +29,44 @@ customResourceState: The following metrics are exposed with this kube-state-metrics configuration: #### Image -``` -# HELP garm_operator_image_created Unix creation timestamp. -# TYPE garm_operator_image_created gauge - -# HELP garm_operator_image_info Information about an image. -# TYPE garm_operator_image_info info -``` +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:----------------------------|:-----------------------| +`garm_operator_image_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_image_info` | Info | Information about an image. | | +
#### Repository -``` -# HELP garm_operator_repo_created Unix creation timestamp. -# TYPE garm_operator_repo_created gauge - -# HELP garm_operator_repo_pool_manager_running Whether the repositories poolManager is running. -# TYPE garm_operator_repo_pool_manager_running gauge - -# HELP garm_operator_repo_info Information about a repository. -# TYPE garm_operator_repo_info info - -# HELP garm_operator_repo_annotation_paused_info Whether the repo reconciliation is paused. -# TYPE garm_operator_repo_annotation_paused_info info -``` +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:-------------------------------------------------|:-----------------------| +`garm_operator_repo_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_repo_pool_manager_running` | Gauge | Whether the repositories poolManager is running. | | +`garm_operator_repo_info` | Info | Information about a repository. | | +`garm_operator_repo_annotation_paused_info` | Info | Whether the repo reconciliation is paused. | | +
#### Organization -``` -# HELP garm_operator_org_created Unix creation timestamp. -# TYPE garm_operator_org_created gauge - -# HELP garm_operator_org_pool_manager_running Whether the orgs poolManager is running. -# TYPE garm_operator_org_pool_manager_running gauge - -# HELP garm_operator_org_info Information about an enterprise. -# TYPE garm_operator_org_info info - -# HELP garm_operator_org_annotation_paused_info Whether the org reconciliation is paused. -# TYPE garm_operator_org_annotation_paused_info info -``` +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:------------------------------------------|:-----------------------| +`garm_operator_org_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_org_pool_manager_running` | Gauge | Whether the orgs poolManager is running. | | +`garm_operator_org_info` | Info | Information about an organization. | | +`garm_operator_org_annotation_paused_info` | Info | Whether the org reconciliation is paused. | | +
#### Enterprise -``` -# HELP garm_operator_enterprise_created Unix creation timestamp. -# TYPE garm_operator_enterprise_created gauge - -# HELP garm_operator_enterprise_pool_manager_running Whether the enterprises poolManager is running. -# TYPE garm_operator_enterprise_pool_manager_running gauge - -# HELP garm_operator_enterprise_info Information about an enterprise. -# TYPE garm_operator_enterprise_info info - -# HELP garm_operator_enterprise_annotation_paused_info Whether the enterprise reconciliation is paused. -# TYPE garm_operator_enterprise_annotation_paused_info info -``` +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:-------------------------------------------------|:-----------------------| +`garm_operator_enterprise_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_enterprise_pool_manager_running` | Gauge | Whether the enterprises poolManager is running. | | +`garm_operator_enterprise_info` | Info | Information about an enterprise. | | +`garm_operator_enterprise_annotation_paused_info` | Info | Whether the enterprise reconciliation is paused. | | +
#### Pool -``` -# HELP garm_operator_pool_created Unix creation timestamp. -# TYPE garm_operator_pool_created gauge +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:-------------------------------------------|:-----------------------| +`garm_operator_pool_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_pool_info` | Gauge | Information about a pool. | | +`garm_operator_pool_annotation_paused_info` | Info | Whether the pool reconciliation is paused. | | -# HELP garm_operator_pool_info Information about a pool. -# TYPE garm_operator_pool_info info - -# HELP garm_operator_pool_annotation_paused_info Whether the pool reconciliation is paused. -# TYPE garm_operator_pool_annotation_paused_info info -``` From 654d5af826ecf4fdf2d17d287154b2193e8a29cc Mon Sep 17 00:00:00 2001 From: rthalho Date: Wed, 28 Feb 2024 15:30:34 +0100 Subject: [PATCH 5/5] fix: add new status conditions metrics --- config/kube-state-metrics/configmap.yaml | 107 +++++++++++++----- .../kube-state-metrics-config.md | 105 ++++++++++++----- 2 files changed, 154 insertions(+), 58 deletions(-) diff --git a/config/kube-state-metrics/configmap.yaml b/config/kube-state-metrics/configmap.yaml index 22f39f51..d76d3dbc 100644 --- a/config/kube-state-metrics/configmap.yaml +++ b/config/kube-state-metrics/configmap.yaml @@ -11,6 +11,9 @@ data: kind: CustomResourceStateMetrics spec: resources: + ################## + # ENTERPRISE # + ################# - groupVersionKind: group: garm-operator.mercedes-benz.com kind: "Enterprise" @@ -30,15 +33,7 @@ data: - metadata - creationTimestamp type: Gauge - - - name: enterprise_pool_manager_running - help: Whether the enterprises poolManager is running. - each: - gauge: - nilIsZero: true - path: [ status, poolManagerIsRunning ] - type: Gauge - + - name: enterprise_info help: Information about an enterprise. each: @@ -61,7 +56,24 @@ data: - garm-operator.mercedes-benz.com/paused labelsFromPath: paused_value: [] - + + - name: enterprise_status_conditions + help: Displays whether status of each possible condition is True or False. + each: + type: Gauge + gauge: + path: + - status + - conditions + valueFrom: + - status + labelFromKey: reason + labelsFromPath: + type: [ type ] + + ################## + # Org # + ################# - groupVersionKind: group: garm-operator.mercedes-benz.com kind: "Organization" @@ -81,15 +93,7 @@ data: - metadata - creationTimestamp type: Gauge - - - name: org_pool_manager_running - help: Whether the orgs poolManager is running. - each: - gauge: - nilIsZero: true - path: [ status, poolManagerIsRunning ] - type: Gauge - + - name: org_info help: Information about an organization. each: @@ -112,7 +116,24 @@ data: - garm-operator.mercedes-benz.com/paused labelsFromPath: paused_value: [ ] - + + - name: org_status_conditions + help: Displays whether status of each possible condition is True or False. + each: + type: Gauge + gauge: + path: + - status + - conditions + valueFrom: + - status + labelFromKey: reason + labelsFromPath: + type: [ type ] + + ################## + # Repo # + ################# - groupVersionKind: group: garm-operator.mercedes-benz.com kind: "Repository" @@ -132,15 +153,7 @@ data: - metadata - creationTimestamp type: Gauge - - - name: repo_pool_manager_running - help: Whether the repositories poolManager is running. - each: - gauge: - nilIsZero: true - path: [ status, poolManagerIsRunning ] - type: Gauge - + - name: repo_info help: Information about a repository. each: @@ -164,7 +177,24 @@ data: - garm-operator.mercedes-benz.com/paused labelsFromPath: paused_value: [ ] - + + - name: repo_status_conditions + help: Displays whether status of each possible condition is True or False. + each: + type: Gauge + gauge: + path: + - status + - conditions + valueFrom: + - status + labelFromKey: reason + labelsFromPath: + type: [ type ] + + ################## + # Pool # + ################# - groupVersionKind: group: garm-operator.mercedes-benz.com kind: "Pool" @@ -219,6 +249,23 @@ data: labelsFromPath: paused_value: [ ] + - name: pool_status_conditions + help: Displays whether status of each possible condition is True or False. + each: + type: Gauge + gauge: + path: + - status + - conditions + valueFrom: + - status + labelFromKey: reason + labelsFromPath: + type: [ type ] + + ################## + # Image # + ################# - groupVersionKind: group: garm-operator.mercedes-benz.com kind: "Image" diff --git a/docs/kube-state-metrics/kube-state-metrics-config.md b/docs/kube-state-metrics/kube-state-metrics-config.md index a643f146..6b029d06 100644 --- a/docs/kube-state-metrics/kube-state-metrics-config.md +++ b/docs/kube-state-metrics/kube-state-metrics-config.md @@ -28,45 +28,94 @@ customResourceState: The following metrics are exposed with this kube-state-metrics configuration: -#### Image +### Image Metric name | Type | Description | Unit (where applicable) | -:-----------|:------|:----------------------------|:-----------------------| -`garm_operator_image_created` | Gauge | Unix creation timestamp. | seconds | -`garm_operator_image_info` | Info | Information about an image. | | +:-----------|:------|:----------------------------|:------------------------| +`garm_operator_image_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_image_info` | Info | Information about an image. | | + +**Example** +``` +garm_operator_image_created{crd_type="image",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Image",customresource_version="v1alpha1",name="runner-default"} 1.708425683e+09 +garm_operator_image_info{crd_type="image",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Image",customresource_version="v1alpha1",name="runner-default",tag="runner:linux-ubuntu-22.04-arm64"} 1 +```
-#### Repository -Metric name | Type | Description | Unit (where applicable) | -:-----------|:------|:-------------------------------------------------|:-----------------------| -`garm_operator_repo_created` | Gauge | Unix creation timestamp. | seconds | -`garm_operator_repo_pool_manager_running` | Gauge | Whether the repositories poolManager is running. | | -`garm_operator_repo_info` | Info | Information about a repository. | | -`garm_operator_repo_annotation_paused_info` | Info | Whether the repo reconciliation is paused. | | +### Repository +Metric name | Type | Description | Unit (where applicable) | +:-----------|:------|:---------------------------------------------------------------------|:------------------------| +`garm_operator_repo_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_repo_info` | Info | Information about a repository. | | +`garm_operator_repo_annotation_paused_info` | Info | Whether the repo reconciliation is paused. | | +`garm_operator_repo_status_conditions` | Gauge | Displays whether status of each possible condition is True or False. | | + +**Example** +``` +garm_operator_repo_created{crd_type="repository",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",name="my-repo"} 1.709127881e+09 +garm_operator_repo_info{crd_type="repository",credentialsName="github-pat",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",id="b33b7f1c-9637-40b5-813f-bba9ec72e92f",name="my-repo",owner="cnt-dev",webhookSecretRefKey="webhookSecret",webhookSecretRefName="org-webhook-secret"} 1 +garm_operator_repo_annotation_paused_info{crd_type="repository",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",name="my-repo",paused_value="true"} 1 + +garm_operator_repo_status_conditions{crd_type="repository",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",name="my-repo",type="PoolManager"} 1 +garm_operator_repo_status_conditions{crd_type="repository",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",name="rthalho-test",type="Ready"} 1 +garm_operator_repo_status_conditions{crd_type="repository",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Repository",customresource_version="v1alpha1",name="rthalho-test",type="SecretReference"} 1 +```
-#### Organization +### Organization Metric name | Type | Description | Unit (where applicable) | -:-----------|:------|:------------------------------------------|:-----------------------| -`garm_operator_org_created` | Gauge | Unix creation timestamp. | seconds | -`garm_operator_org_pool_manager_running` | Gauge | Whether the orgs poolManager is running. | | -`garm_operator_org_info` | Info | Information about an organization. | | -`garm_operator_org_annotation_paused_info` | Info | Whether the org reconciliation is paused. | | +:-----------|:------|:------------------------------------------|:------------------------| +`garm_operator_org_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_org_info` | Info | Information about an organization. | | +`garm_operator_org_annotation_paused_info` | Info | Whether the org reconciliation is paused. | | +`garm_operator_org_status_conditions` | Gauge | Displays whether status of each possible condition is True or False. | | + +**Example** +``` +garm_operator_org_created{crd_type="organization",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",name="github-actions"} 1.708425683e+09 +garm_operator_org_info{crd_type="organization",credentialsName="github-pat",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",id="3a407238-4599-457f-a9f9-dfe0b294219d",name="github-actions",webhookSecretRefKey="webhookSecret",webhookSecretRefName="org-webhook-secret"} 1 +garm_operator_org_annotation_paused_info{crd_type="organization",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",name="github-actions",paused_value="true"} 1 + +garm_operator_org_status_conditions{crd_type="organization",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",name="github-actions",type="PoolManager"} 1 +garm_operator_org_status_conditions{crd_type="organization",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",name="github-actions",type="Ready"} 1 +garm_operator_org_status_conditions{crd_type="organization",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Organization",customresource_version="v1alpha1",name="github-actions",type="SecretReference"} 1 +```
-#### Enterprise +### Enterprise Metric name | Type | Description | Unit (where applicable) | -:-----------|:------|:-------------------------------------------------|:-----------------------| -`garm_operator_enterprise_created` | Gauge | Unix creation timestamp. | seconds | -`garm_operator_enterprise_pool_manager_running` | Gauge | Whether the enterprises poolManager is running. | | -`garm_operator_enterprise_info` | Info | Information about an enterprise. | | -`garm_operator_enterprise_annotation_paused_info` | Info | Whether the enterprise reconciliation is paused. | | +:-----------|:------|:-------------------------------------------------|:------------------------| +`garm_operator_enterprise_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_enterprise_info` | Info | Information about an enterprise. | | +`garm_operator_enterprise_annotation_paused_info` | Info | Whether the enterprise reconciliation is paused. | | +`garm_operator_enterprise_status_conditions` | Gauge | Displays whether status of each possible condition is True or False. | | + +**Example** +``` +garm_operator_enterprise_created{crd_type="enterprise",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",name="mercedes-benz-ag"} 1.708425683e+09 +garm_operator_enterprise_info{crd_type="enterprise",credentialsName="github-pat",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",id="3a407238-4599-457f-a9f9-dfe0b294219d",name="mercedes-benz-ag",webhookSecretRefKey="webhookSecret",webhookSecretRefName="enterprise-webhook-secret"} 1 +garm_operator_enterprise_annotation_paused_info{crd_type="enterprise",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",name="mercedes-benz-ag",paused_value="true"} 1 + +garm_operator_enterprise_status_conditions{crd_type="enterprise",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",name="mercedes-benz-ag",type="PoolManager"} 1 +garm_operator_enterprise_status_conditions{crd_type="enterprise",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",name="mercedes-benz-ag",type="Ready"} 1 +garm_operator_enterprise_status_conditions{crd_type="enterprise",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Enterprise",customresource_version="v1alpha1",name="mercedes-benz-ag",type="SecretReference"} 1 +```
-#### Pool +### Pool Metric name | Type | Description | Unit (where applicable) | -:-----------|:------|:-------------------------------------------|:-----------------------| -`garm_operator_pool_created` | Gauge | Unix creation timestamp. | seconds | -`garm_operator_pool_info` | Gauge | Information about a pool. | | -`garm_operator_pool_annotation_paused_info` | Info | Whether the pool reconciliation is paused. | | +:-----------|:------|:-------------------------------------------|:------------------------| +`garm_operator_pool_created` | Gauge | Unix creation timestamp. | seconds | +`garm_operator_pool_info` | Gauge | Information about a pool. | | | +`garm_operator_pool_annotation_paused_info` | Info | Whether the pool reconciliation is paused. | | +`garm_operator_repo_status_conditions` | Gauge | Displays whether status of each possible condition is True or False. | | +**Example** +``` +garm_operator_pool_created{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",name="kubernetes-pool-small-org-github-actions"} 1.708425683e+09 +garm_operator_pool_info{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",enabled="true",githubRunnerGroup="",id="1e52ec4c-0b78-4451-998b-40092344e5a9",imageName="runner-default-1",longRunningIdleRunners="0",maxRunners="2",minIdleRunners="0",name="kubernetes-pool-medium-org-github-actions",osArch="amd64",osType="linux",providerName="kubernetes_external",runnerBootstrapTimeout="2",runnerPrefix="road-runner-k8s-rthalho",scopeKind="Organization",scopeName="github-actions",tags="[medium k8s-dev garm-operator-dev]"} 1 +garm_operator_pool_annotation_paused_info{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",name="kubernetes-pool-medium-org-github-actions",paused_value="true"} 1 +garm_operator_pool_status_conditions{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",name="kubernetes-pool-medium-org-github-actions",type="ImageReference"} 1 +garm_operator_pool_status_conditions{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",name="kubernetes-pool-medium-org-github-actions",type="Ready"} 1 +garm_operator_pool_status_conditions{crd_type="pool",customresource_group="garm-operator.mercedes-benz.com",customresource_kind="Pool",customresource_version="v1alpha1",name="kubernetes-pool-medium-org-github-actions",type="ScopeReference"} 1 +```