From fa280679b7973433871590473ece219364acefa6 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 12 Mar 2024 12:18:06 +0100 Subject: [PATCH 1/4] feat: add observability ui plugins api --- ...bility-operator.clusterserviceversion.yaml | 74 +++++++ ...bilityui.rhobs_observabilityuiplugins.yaml | 122 +++++++++++ cmd/operator/main.go | 8 +- deploy/crds/common/kustomization.yaml | 1 + ...bilityui.rhobs_observabilityuiplugins.yaml | 117 ++++++++++ ...bility-operator.clusterserviceversion.yaml | 5 + .../observability-operator-cluster-role.yaml | 70 ++++++ docs/api.md | 188 ++++++++++++++++ docs/user-guides/observability-ui-plugins.md | 23 ++ go.mod | 2 + go.sum | 6 +- .../observability-ui/v1alpha1/register.go | 40 ++++ pkg/apis/observability-ui/v1alpha1/types.go | 120 +++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 138 ++++++++++++ .../compatibility_matrix.go | 52 +++++ .../compatibility_matrix_test.go | 43 ++++ .../observability-ui-plugin/components.go | 198 +++++++++++++++++ .../observability-ui-plugin/controller.go | 203 ++++++++++++++++++ .../plugin_info_builder.go | 105 +++++++++ pkg/operator/operator.go | 28 ++- pkg/operator/scheme.go | 9 +- pkg/reconciler/reconciler.go | 17 ++ 22 files changed, 1556 insertions(+), 13 deletions(-) create mode 100644 bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml create mode 100644 deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml create mode 100644 docs/user-guides/observability-ui-plugins.md create mode 100644 pkg/apis/observability-ui/v1alpha1/register.go create mode 100644 pkg/apis/observability-ui/v1alpha1/types.go create mode 100644 pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go create mode 100644 pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go create mode 100644 pkg/controllers/observability-ui/observability-ui-plugin/components.go create mode 100644 pkg/controllers/observability-ui/observability-ui-plugin/controller.go create mode 100644 pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go diff --git a/bundle/manifests/observability-operator.clusterserviceversion.yaml b/bundle/manifests/observability-operator.clusterserviceversion.yaml index f58bb068e..a008f50e7 100644 --- a/bundle/manifests/observability-operator.clusterserviceversion.yaml +++ b/bundle/manifests/observability-operator.clusterserviceversion.yaml @@ -75,6 +75,9 @@ spec: kind: MonitoringStack name: monitoringstacks.monitoring.rhobs version: v1alpha1 + - kind: ObservabilityUIPlugin + name: observabilityuiplugins.observabilityui.rhobs + version: v1alpha1 - description: PodMonitor defines monitoring for a set of pods displayName: PodMonitor kind: PodMonitor @@ -262,6 +265,20 @@ spec: - use serviceAccountName: obo-prometheus-operator-admission-webhook - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -296,6 +313,26 @@ spec: - patch - update - watch + - apiGroups: + - config.openshift.io + resources: + - clusterversions + verbs: + - get + - list + - watch + - apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -391,6 +428,42 @@ spec: - get - patch - update + - apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins/finalizers + - observabilityuiplugins/status + verbs: + - get + - update + - apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins/status + verbs: + - get + - update + - apiGroups: + - operator.openshift.io + resources: + - consoles + verbs: + - get + - list + - patch + - watch - apiGroups: - policy resources: @@ -603,6 +676,7 @@ spec: - --images=alertmanager=quay.io/prometheus/alertmanager:v0.26.0 - --images=prometheus=quay.io/prometheus/prometheus:v2.49.1 - --images=thanos=quay.io/thanos/thanos:v0.33.0 + - --images=ui-dashboards=quay.io/openshift-observability-ui/console-dashboards-plugin:v0.1.0 env: - name: NAMESPACE valueFrom: diff --git a/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml b/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml new file mode 100644 index 000000000..6d105f303 --- /dev/null +++ b/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml @@ -0,0 +1,122 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: observabilityuiplugins.observabilityui.rhobs +spec: + group: observabilityui.rhobs + names: + kind: ObservabilityUIPlugin + listKind: ObservabilityUIPluginList + plural: observabilityuiplugins + singular: observabilityuiplugin + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ObservabilityUIPlugin defines an observability console plugin + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Observabilityuipluginpec is the specification for desired + state of ObservabilityUIPlugin. + properties: + type: + enum: + - dashboards + type: string + required: + - type + type: object + status: + description: Observabilityuiplugintatus defines the observed state of + ObservabilityUIPlugin. It should always be reconstructable from the + state of the cluster and/or outside world. + properties: + conditions: + description: Conditions provide status information about the plugin. + items: + 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 + enum: + - "True" + - "False" + - Unknown + - Degraded + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + 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 + x-kubernetes-list-type: atomic + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 31461d562..ccff01947 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -36,9 +36,10 @@ import ( // prometheus-operator. For thanos we use the default version from // prometheus-operator. var defaultImages = map[string]string{ - "prometheus": "", - "alertmanager": "", - "thanos": obopo.DefaultThanosImage, + "prometheus": "", + "alertmanager": "", + "thanos": obopo.DefaultThanosImage, + "ui-dashboards": "quay.io/openshift-observability-ui/console-dashboards-plugin:v0.1.0", } func imagesUsed() []string { @@ -112,6 +113,7 @@ func main() { operator.WithAlertmanagerImage(imgMap["alertmanager"]), operator.WithThanosSidecarImage(imgMap["thanos"]), operator.WithThanosQuerierImage(imgMap["thanos"]), + operator.WithUIPluginImages(imgMap), )) if err != nil { setupLog.Error(err, "cannot create a new operator") diff --git a/deploy/crds/common/kustomization.yaml b/deploy/crds/common/kustomization.yaml index 710288ba7..f4b7aab02 100644 --- a/deploy/crds/common/kustomization.yaml +++ b/deploy/crds/common/kustomization.yaml @@ -4,3 +4,4 @@ kind: Kustomization resources: - monitoring.rhobs_monitoringstacks.yaml - monitoring.rhobs_thanosqueriers.yaml + - observabilityui.rhobs_observabilityuiplugins.yaml diff --git a/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml b/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml new file mode 100644 index 000000000..ad81b49ba --- /dev/null +++ b/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: observabilityuiplugins.observabilityui.rhobs +spec: + group: observabilityui.rhobs + names: + kind: ObservabilityUIPlugin + listKind: ObservabilityUIPluginList + plural: observabilityuiplugins + singular: observabilityuiplugin + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ObservabilityUIPlugin defines an observability console plugin + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Observabilityuipluginpec is the specification for desired + state of ObservabilityUIPlugin. + properties: + type: + enum: + - dashboards + type: string + required: + - type + type: object + status: + description: Observabilityuiplugintatus defines the observed state of + ObservabilityUIPlugin. It should always be reconstructable from the + state of the cluster and/or outside world. + properties: + conditions: + description: Conditions provide status information about the plugin. + items: + 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 + enum: + - "True" + - "False" + - Unknown + - Degraded + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + 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 + x-kubernetes-list-type: atomic + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/olm/bases/observability-operator.clusterserviceversion.yaml b/deploy/olm/bases/observability-operator.clusterserviceversion.yaml index 338bc17e1..d61f42599 100644 --- a/deploy/olm/bases/observability-operator.clusterserviceversion.yaml +++ b/deploy/olm/bases/observability-operator.clusterserviceversion.yaml @@ -85,6 +85,11 @@ spec: kind: ThanosRuler name: thanosrulers.monitoring.rhobs version: v1 + - description: ObservabilityUIPlugin defines a console plugin for observability + displayName: ObservabilityUIPlugin + kind: ObservabilityUIPlugin + name: observabilityui.rhobs + version: v1alpha1 description: >+ Observability Operator is a Go based Kubernetes operator to setup and diff --git a/deploy/operator/observability-operator-cluster-role.yaml b/deploy/operator/observability-operator-cluster-role.yaml index 43921b138..b395cbc4e 100644 --- a/deploy/operator/observability-operator-cluster-role.yaml +++ b/deploy/operator/observability-operator-cluster-role.yaml @@ -4,6 +4,20 @@ kind: ClusterRole metadata: name: observability-operator rules: +- apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -38,6 +52,26 @@ rules: - patch - update - watch +- apiGroups: + - config.openshift.io + resources: + - clusterversions + verbs: + - get + - list + - watch +- apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -133,6 +167,42 @@ rules: - get - patch - update +- apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins/finalizers + - observabilityuiplugins/status + verbs: + - get + - update +- apiGroups: + - observabilityui.rhobs + resources: + - observabilityuiplugins/status + verbs: + - get + - update +- apiGroups: + - operator.openshift.io + resources: + - consoles + verbs: + - get + - list + - patch + - watch - apiGroups: - policy resources: diff --git a/docs/api.md b/docs/api.md index 37be3e93e..f422d05e9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,6 +3,7 @@ Packages: - [monitoring.rhobs/v1alpha1](#monitoringrhobsv1alpha1) +- [observabilityui.rhobs/v1alpha1](#observabilityuirhobsv1alpha1) # monitoring.rhobs/v1alpha1 @@ -2765,4 +2766,191 @@ list restricting them.
false + + +# observabilityui.rhobs/v1alpha1 + +Resource Types: + +- [ObservabilityUIPlugin](#observabilityuiplugin) + + + + +## ObservabilityUIPlugin +[↩ Parent](#observabilityuirhobsv1alpha1 ) + + + + + + +ObservabilityUIPlugin defines an observability console plugin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringobservabilityui.rhobs/v1alpha1true
kindstringObservabilityUIPlugintrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin.
+
false
statusobject + Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. It should always be reconstructable from the state of the cluster and/or outside world.
+
false
+ + +### ObservabilityUIPlugin.spec +[↩ Parent](#observabilityuiplugin) + + + +Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
typeenum +
+
+ Enum: dashboards
+
true
+ + +### ObservabilityUIPlugin.status +[↩ Parent](#observabilityuiplugin) + + + +Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. It should always be reconstructable from the state of the cluster and/or outside world. + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
conditions[]object + Conditions provide status information about the plugin.
+
true
+ + +### ObservabilityUIPlugin.status.conditions[index] +[↩ Parent](#observabilityuipluginstatus) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
lastTransitionTimestring + 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
+
true
messagestring + message is a human readable message indicating details about the transition. This may be an empty string.
+
true
reasonstring + 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.
+
true
statusenum + status of the condition
+
+ Enum: True, False, Unknown, Degraded
+
true
typestring + type of condition in CamelCase or in foo.example.com/CamelCase. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+
true
observedGenerationinteger + 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
+
false
\ No newline at end of file diff --git a/docs/user-guides/observability-ui-plugins.md b/docs/user-guides/observability-ui-plugins.md new file mode 100644 index 000000000..6cef19c92 --- /dev/null +++ b/docs/user-guides/observability-ui-plugins.md @@ -0,0 +1,23 @@ +# Observability UI Plugins + +Using the Observability UI, you can install and manage plugins that extend the observability functionality of the OpenShift web console. Plugins are installed and managed by the Observability Operator. + +## Plugins + +- [dashboards](#dashboards): Add enhanced dashboards to the OpenShift web console. This plugin allows you to add other prometheus datasources present in the cluster, apart from the in-cluster one, to the default dashboards. + +### Dashboards + +The plugin will search for datasources as ConfigMaps in the `console-dashboards` namespace with the `console.openshift.io/dashboard-datasource: 'true'` label. The namespace `console-dashboards` is required, more details on how to create a datasource ConfigMap can be found in the [console-dashboards-plugin](https://github.com/openshift/console-dashboards-plugin/blob/main/docs/add-datasource.md) + +To enable the console dashboards plugin, create a `ObservabilityUIPlugin` CR. The following example shows how to create a CR to enable the console dashboards plugin: + +```yaml +apiVersion: observabilityui.rhobs/v1alpha1 +kind: ObservabilityUIPlugin +metadata: + name: ui-dashboards + namespace: openshift-observability-ui +spec: + type: Dashboards +``` diff --git a/go.mod b/go.mod index 1d3dc2bcd..361e52fac 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.21 require ( github.com/go-logr/logr v1.4.1 github.com/google/go-cmp v0.6.0 + github.com/openshift/api v0.0.0-20240301093301-ce10821dc999 github.com/pkg/errors v0.9.1 github.com/prometheus/common v0.51.1 github.com/rhobs/obo-prometheus-operator v0.71.0-rhobs1 github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring v0.71.0-rhobs1 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20231127185646-65229373498e + golang.org/x/mod v0.16.0 gotest.tools/v3 v3.5.1 k8s.io/api v0.29.3 k8s.io/apiextensions-apiserver v0.29.3 diff --git a/go.sum b/go.sum index 9a02094ba..207588d76 100644 --- a/go.sum +++ b/go.sum @@ -394,6 +394,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/openshift/api v0.0.0-20240301093301-ce10821dc999 h1:+S998xHiJApsJZjRAO8wyedU9GfqFd8mtwWly6LqHDo= +github.com/openshift/api v0.0.0-20240301093301-ce10821dc999/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= @@ -558,8 +560,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/pkg/apis/observability-ui/v1alpha1/register.go b/pkg/apis/observability-ui/v1alpha1/register.go new file mode 100644 index 000000000..848c78812 --- /dev/null +++ b/pkg/apis/observability-ui/v1alpha1/register.go @@ -0,0 +1,40 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains API Schema definitions for the rhobs v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=observabilityui.rhobs +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "observabilityui.rhobs", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register(&ObservabilityUIPlugin{}, &ObservabilityUIPluginList{}) +} diff --git a/pkg/apis/observability-ui/v1alpha1/types.go b/pkg/apis/observability-ui/v1alpha1/types.go new file mode 100644 index 000000000..7b555c4d5 --- /dev/null +++ b/pkg/apis/observability-ui/v1alpha1/types.go @@ -0,0 +1,120 @@ +// +groupName=observabilityui.rhobs +// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins,verbs=list;get;watch +// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins/status;observabilityuiplugins/finalizers,verbs=get;update + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ObservabilityUIPlugin defines an observability console plugin +// +k8s:openapi-gen=true +// +kubebuilder:resource +// +kubebuilder:subresource:status +type ObservabilityUIPlugin struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ObservabilityUIPluginSpec `json:"spec,omitempty"` + Status ObservabilityUIPluginStatus `json:"status,omitempty"` +} + +// ObservabilityUIPluginList contains a list of ObservabilityUIPlugin +// +kubebuilder:resource +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ObservabilityUIPluginList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ObservabilityUIPlugin `json:"items"` +} + +type UIPluginType string + +const ( + TypeDashboards UIPluginType = "dashboards" +) + +// Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin. +type ObservabilityUIPluginSpec struct { + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=dashboards + Type UIPluginType `json:"type"` +} + +// Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. +// It should always be reconstructable from the state of the cluster and/or outside world. +type ObservabilityUIPluginStatus struct { + // Conditions provide status information about the plugin. + // +listType=atomic + Conditions []Condition `json:"conditions"` +} + +type ConditionStatus string + +// +required +// +kubebuilder:validation:Required +// +kubebuilder:validation: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])$` +// +kubebuilder:validation:MaxLength=316 +type ConditionType string + +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" + + ReconciledCondition ConditionType = "Reconciled" + AvailableCondition ConditionType = "Available" + ResourceDiscoveryCondition ConditionType = "ResourceDiscovery" +) + +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + Type ConditionType `json:"type"` + // 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. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // 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. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // 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. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` + // status of the condition + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown;Degraded + Status ConditionStatus `json:"status"` +} + +func (c Condition) Equal(n Condition) bool { + if c.Reason == n.Reason && c.Status == n.Status && c.Message == n.Message && c.ObservedGeneration == n.ObservedGeneration { + return true + } + return false +} diff --git a/pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..008ec4d78 --- /dev/null +++ b/pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,138 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilityUIPlugin) DeepCopyInto(out *ObservabilityUIPlugin) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPlugin. +func (in *ObservabilityUIPlugin) DeepCopy() *ObservabilityUIPlugin { + if in == nil { + return nil + } + out := new(ObservabilityUIPlugin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ObservabilityUIPlugin) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilityUIPluginList) DeepCopyInto(out *ObservabilityUIPluginList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ObservabilityUIPlugin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginList. +func (in *ObservabilityUIPluginList) DeepCopy() *ObservabilityUIPluginList { + if in == nil { + return nil + } + out := new(ObservabilityUIPluginList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ObservabilityUIPluginList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilityUIPluginSpec) DeepCopyInto(out *ObservabilityUIPluginSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginSpec. +func (in *ObservabilityUIPluginSpec) DeepCopy() *ObservabilityUIPluginSpec { + if in == nil { + return nil + } + out := new(ObservabilityUIPluginSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilityUIPluginStatus) DeepCopyInto(out *ObservabilityUIPluginStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginStatus. +func (in *ObservabilityUIPluginStatus) DeepCopy() *ObservabilityUIPluginStatus { + if in == nil { + return nil + } + out := new(ObservabilityUIPluginStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go new file mode 100644 index 000000000..4a09746ef --- /dev/null +++ b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go @@ -0,0 +1,52 @@ +package observability_ui_plugin + +import ( + "strings" + + "golang.org/x/mod/semver" + + obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" +) + +type CompatibilityEntry struct { + PluginType obsui.UIPluginType + MinClusterVersion string + MaxClusterVersion string + ImageKey string + Features []string +} + +var compatibilityMatrix = []CompatibilityEntry{ + { + PluginType: obsui.TypeDashboards, + MinClusterVersion: "v4.11", + MaxClusterVersion: "", + ImageKey: "ui-dashboards", + Features: []string{}, + }, +} + +func getImageKeyForPluginType(pluginType obsui.UIPluginType, clusterVersion string) string { + if !strings.HasPrefix(clusterVersion, "v") { + clusterVersion = "v" + clusterVersion + } + + // No console plugins are supported before 4.11 + if semver.Compare(clusterVersion, "v4.11") < 0 { + return "" + } + + for _, entry := range compatibilityMatrix { + if entry.PluginType == pluginType { + if entry.MaxClusterVersion == "" && semver.Compare(clusterVersion, entry.MinClusterVersion) >= 0 { + return entry.ImageKey + } + + if semver.Compare(clusterVersion, entry.MinClusterVersion) >= 0 && semver.Compare(clusterVersion, entry.MaxClusterVersion) < 0 { + return entry.ImageKey + } + } + } + + return "" +} diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go new file mode 100644 index 000000000..253e4842f --- /dev/null +++ b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go @@ -0,0 +1,43 @@ +package observability_ui_plugin + +import ( + "testing" + + obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + + "gotest.tools/v3/assert" +) + +func TestCompatibilityMatrixSpec(t *testing.T) { + tt := []struct { + pluginType obsui.UIPluginType + clusterVersion string + expectedKey string + }{ + { + pluginType: obsui.TypeDashboards, + clusterVersion: "4.10", + expectedKey: "", + }, + { + pluginType: obsui.TypeDashboards, + clusterVersion: "4.11", + expectedKey: "ui-dashboards", + }, + { + pluginType: obsui.TypeDashboards, + clusterVersion: "4.24.0-0.nightly-2024-03-11-200348", + expectedKey: "ui-dashboards", + }, + { + pluginType: "non-existent-plugin", + clusterVersion: "4.24.0-0.nightly-2024-03-11-200348", + expectedKey: "", + }, + } + + for _, tc := range tt { + actualKey := getImageKeyForPluginType(tc.pluginType, tc.clusterVersion) + assert.DeepEqual(t, tc.expectedKey, actualKey) + } +} diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/components.go b/pkg/controllers/observability-ui/observability-ui-plugin/components.go new file mode 100644 index 000000000..adef8916e --- /dev/null +++ b/pkg/controllers/observability-ui/observability-ui-plugin/components.go @@ -0,0 +1,198 @@ +package observability_ui_plugin + +import ( + "fmt" + + "github.com/rhobs/observability-operator/pkg/reconciler" + + osv1alpha1 "github.com/openshift/api/console/v1alpha1" + obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func pluginComponentReconcilers(plugin *obsui.ObservabilityUIPlugin, pluginInfo ObservabilityUIPluginInfo) []reconciler.Reconciler { + hasClusterRole := pluginInfo.ClusterRole != nil + hasClusterRoleBinding := pluginInfo.ClusterRoleBinding != nil + namespace := plugin.Namespace + + return []reconciler.Reconciler{ + reconciler.NewUpdater(newServiceAccount(pluginInfo, namespace), plugin), + reconciler.NewOptionalUpdater(newClusterRole(pluginInfo), plugin, hasClusterRole), + reconciler.NewOptionalUpdater(newClusterRoleBinding(pluginInfo), plugin, hasClusterRoleBinding), + reconciler.NewUpdater(newDeployment(pluginInfo, namespace), plugin), + reconciler.NewUpdater(newService(pluginInfo, namespace), plugin), + reconciler.NewUpdater(newConsolePlugin(pluginInfo, namespace), plugin), + } +} + +func newServiceAccount(info ObservabilityUIPluginInfo, namespace string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: info.Name + "-sa", + Namespace: namespace, + }, + } +} + +func newClusterRole(info ObservabilityUIPluginInfo) *rbacv1.ClusterRole { + return info.ClusterRole +} + +func newClusterRoleBinding(info ObservabilityUIPluginInfo) *rbacv1.ClusterRoleBinding { + return info.ClusterRoleBinding +} + +func newConsolePlugin(info ObservabilityUIPluginInfo, namespace string) *osv1alpha1.ConsolePlugin { + return &osv1alpha1.ConsolePlugin{ + TypeMeta: metav1.TypeMeta{ + APIVersion: osv1alpha1.SchemeGroupVersion.String(), + Kind: "ConsolePlugin", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: info.ConsoleName, + }, + Spec: osv1alpha1.ConsolePluginSpec{ + DisplayName: info.DisplayName, + Service: osv1alpha1.ConsolePluginService{ + Name: info.Name, + Namespace: namespace, + Port: 9443, + BasePath: "/", + }, + Proxy: info.Proxies, + }, + } +} + +func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Deployment { + plugin := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: info.Name, + Namespace: namespace, + Labels: componentLabels(info.Name), + }, + Spec: appsv1.DeploymentSpec{ + Replicas: func(i int32) *int32 { return &i }(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/instance": info.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: info.Name, + Namespace: namespace, + Labels: componentLabels(info.Name), + }, + Spec: corev1.PodSpec{ + ServiceAccountName: info.Name + "-sa", + Containers: []corev1.Container{ + { + Name: info.Name, + Image: info.Image, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 9443, + Name: "web", + }, + }, + TerminationMessagePolicy: "FallbackToLogsOnError", + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "serving-cert", + ReadOnly: true, + MountPath: "/var/serving-cert", + }, + }, + Args: []string{ + fmt.Sprintf("-port=%d", 9443), + "-cert=/var/serving-cert/tls.crt", + "-key=/var/serving-cert/tls.key", + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "serving-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: info.Name, + DefaultMode: &[]int32{420}[0], + }, + }, + }, + }, + NodeSelector: map[string]string{ + "kubernetes.io/os": "linux", + }, + }, + }, + ProgressDeadlineSeconds: func(i int32) *int32 { return &i }(300), + }, + } + + return plugin +} + +func newService(info ObservabilityUIPluginInfo, namespace string) *corev1.Service { + annotations := map[string]string{ + "service.alpha.openshift.io/serving-cert-secret-name": info.Name, + } + + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: info.Name, + Namespace: namespace, + Labels: componentLabels(info.Name), + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 9443, + Name: "http", + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt32(9443), + }, + }, + Selector: map[string]string{ + "app.kubernetes.io/instance": info.Name, + }, + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func componentLabels(pluginName string) map[string]string { + return map[string]string{ + "app.kubernetes.io/instance": pluginName, + "app.kubernetes.io/part-of": "ObservabilityUIPlugin", + "app.kubernetes.io/managed-by": "observability-operator", + } +} diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/controller.go b/pkg/controllers/observability-ui/observability-ui-plugin/controller.go new file mode 100644 index 000000000..7de3e589f --- /dev/null +++ b/pkg/controllers/observability-ui/observability-ui-plugin/controller.go @@ -0,0 +1,203 @@ +package observability_ui_plugin + +import ( + "context" + "slices" + "time" + + "github.com/go-logr/logr" + configv1 "github.com/openshift/api/config/v1" + osv1alpha1 "github.com/openshift/api/console/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + operatorv1 "github.com/openshift/api/operator/v1" + obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + "github.com/rhobs/observability-operator/pkg/reconciler" +) + +type resourceManager struct { + k8sClient client.Client + scheme *runtime.Scheme + logger logr.Logger + controller controller.Controller + pluginConf ObservabilityUIPluginsConfiguration + clusterVersion string +} + +type ObservabilityUIPluginsConfiguration struct { + Images map[string]string +} + +type Options struct { + PluginsConf ObservabilityUIPluginsConfiguration +} + +// RBAC for managing ObservabilityUIPlugins +// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins,verbs=get;list;watch;create;update;delete;patch +// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins/status,verbs=get;update + +// RBAC for managing observability ui plugin objects +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=serviceaccounts;services;configmaps,verbs=get;list;watch;create;update;patch;delete + +// RBAC for managing Console CRs +// +kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=get;patch;list;watch +// +kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;list;watch;create;update;delete;patch + +// RBAC for reading cluster version +// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch + +func RegisterWithManager(mgr ctrl.Manager, opts Options) error { + logger := ctrl.Log.WithName("observability-ui") + + clusterVersion, err := getClusterVersion(mgr.GetAPIReader()) + + if err != nil { + logger.Error(err, "failed to get cluster version") + } + + rm := &resourceManager{ + k8sClient: mgr.GetClient(), + scheme: mgr.GetScheme(), + logger: logger, + pluginConf: opts.PluginsConf, + clusterVersion: clusterVersion.Status.Desired.Version, + } + + generationChanged := builder.WithPredicates(predicate.GenerationChangedPredicate{}) + + ctrl, err := ctrl.NewControllerManagedBy(mgr). + For(&obsui.ObservabilityUIPlugin{}). + Owns(&appsv1.Deployment{}, generationChanged). + Owns(&v1.Service{}, generationChanged). + Owns(&v1.ServiceAccount{}, generationChanged). + Owns(&rbacv1.ClusterRole{}, generationChanged). + Owns(&rbacv1.ClusterRoleBinding{}, generationChanged). + Owns(&osv1alpha1.ConsolePlugin{}, generationChanged). + Build(rm) + if err != nil { + return err + } + rm.controller = ctrl + + return nil +} + +func getClusterVersion(k8client client.Reader) (*configv1.ClusterVersion, error) { + clusterVersion := &configv1.ClusterVersion{} + key := client.ObjectKey{Name: "version"} + if err := k8client.Get(context.TODO(), key, clusterVersion); err != nil { + return nil, err + } + return clusterVersion, nil +} + +func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := rm.logger.WithValues("plugin", req.NamespacedName) + logger.Info("Reconciling observability UI plugin") + + plugin, err := rm.getUIPlugin(ctx, req) + if err != nil { + // retry since some error has occured + return ctrl.Result{}, err + } + + if plugin == nil { + // no such obs ui plugin, so stop here + return ctrl.Result{}, nil + } + + if !plugin.ObjectMeta.DeletionTimestamp.IsZero() { + logger.V(6).Info("skipping reconcile since object is already schedule for deletion") + return ctrl.Result{}, nil + } + + pluginInfo, err := PluginInfoBuilder(plugin, rm.pluginConf, rm.clusterVersion) + + if err != nil { + logger.Error(err, "failed to reconcile plugin") + return ctrl.Result{}, err + } + + reconcilers := pluginComponentReconcilers(plugin, *pluginInfo) + for _, reconciler := range reconcilers { + err := reconciler.Reconcile(ctx, rm.k8sClient, rm.scheme) + // handle creation / updation errors that can happen due to a stale cache by + // retrying after some time. + if apierrors.IsAlreadyExists(err) || apierrors.IsConflict(err) { + logger.V(8).Info("skipping reconcile error", "err", err) + return ctrl.Result{RequeueAfter: 2 * time.Second}, nil + } + if err != nil { + return ctrl.Result{}, err + } + } + + if err := rm.registerPluginWithConsole(ctx, pluginInfo); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (rm resourceManager) registerPluginWithConsole(ctx context.Context, pluginInfo *ObservabilityUIPluginInfo) error { + cluster := &operatorv1.Console{} + if err := rm.k8sClient.Get(ctx, client.ObjectKey{Name: "cluster"}, cluster); err != nil { + return err + } + + if !slices.Contains(cluster.Spec.Plugins, pluginInfo.ConsoleName) { + // Register the plugin with the console + cluster := &operatorv1.Console{ + TypeMeta: metav1.TypeMeta{ + APIVersion: operatorv1.GroupVersion.String(), + Kind: "Console", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: operatorv1.ConsoleSpec{ + OperatorSpec: operatorv1.OperatorSpec{ + ManagementState: operatorv1.Managed, + }, + Plugins: []string{ + pluginInfo.ConsoleName, + }, + }, + } + + if err := reconciler.NewMerger(cluster).Reconcile(ctx, rm.k8sClient, rm.scheme); err != nil { + return err + } + } + + return nil +} + +func (rm resourceManager) getUIPlugin(ctx context.Context, req ctrl.Request) (*obsui.ObservabilityUIPlugin, error) { + logger := rm.logger.WithValues("plugin", req.NamespacedName) + + plugin := obsui.ObservabilityUIPlugin{} + + if err := rm.k8sClient.Get(ctx, req.NamespacedName, &plugin); err != nil { + if apierrors.IsNotFound(err) { + logger.V(3).Info("stack could not be found; may be marked for deletion") + return nil, nil + } + logger.Error(err, "failed to get ObservabilityUIPlugin") + return nil, err + } + + return &plugin, nil +} diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go b/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go new file mode 100644 index 000000000..dfc5db61e --- /dev/null +++ b/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go @@ -0,0 +1,105 @@ +package observability_ui_plugin + +import ( + "fmt" + + osv1alpha1 "github.com/openshift/api/console/v1alpha1" + obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ObservabilityUIPluginInfo struct { + Image string + Name string + ConsoleName string + DisplayName string + Proxies []osv1alpha1.ConsolePluginProxy + ClusterRole *rbacv1.ClusterRole + ClusterRoleBinding *rbacv1.ClusterRoleBinding +} + +func PluginInfoBuilder(plugin *obsui.ObservabilityUIPlugin, pluginConf ObservabilityUIPluginsConfiguration, clusterVersion string) (*ObservabilityUIPluginInfo, error) { + imageKey := getImageKeyForPluginType(plugin.Spec.Type, clusterVersion) + + if imageKey == "" { + return nil, fmt.Errorf("no compatible image found for plugin type %s and cluster version %s", plugin.Spec.Type, clusterVersion) + } + + image := pluginConf.Images[imageKey] + + if image == "" { + return nil, fmt.Errorf("no image provided for plugin type %s with key %s", plugin.Spec.Type, imageKey) + } + + name := "observability-ui-" + plugin.Name + + switch plugin.Spec.Type { + case "dashboards": + { + readerRoleName := plugin.Name + "-datasource-reader" + + pluginInfo := &ObservabilityUIPluginInfo{ + Image: image, + Name: name, + ConsoleName: "console-dashboards-plugin", + DisplayName: "Console Enhanced Dashboards", + Proxies: []osv1alpha1.ConsolePluginProxy{ + { + Type: osv1alpha1.ProxyTypeService, + Alias: "backend", + Authorize: true, + Service: osv1alpha1.ConsolePluginProxyServiceConfig{ + Name: name, + Namespace: plugin.Namespace, + Port: 9443, + }, + }, + }, + ClusterRole: &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + APIVersion: rbacv1.SchemeGroupVersion.String(), + Kind: "ClusterRole", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: readerRoleName, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ClusterRoleBinding: &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: rbacv1.SchemeGroupVersion.String(), + Kind: "ClusterRoleBinding", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name + "-rolebinding", + }, + Subjects: []rbacv1.Subject{ + { + APIGroup: corev1.SchemeGroupVersion.Group, + Kind: "ServiceAccount", + Name: name + "-sa", + Namespace: plugin.Namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.SchemeGroupVersion.Group, + Kind: "ClusterRole", + Name: readerRoleName, + }, + }, + } + + return pluginInfo, nil + } + } + + return nil, fmt.Errorf("plugin type not supported: %s", plugin.Spec.Type) +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 834769490..b870dc4c9 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -12,6 +14,7 @@ import ( stackctrl "github.com/rhobs/observability-operator/pkg/controllers/monitoring/monitoring-stack" tqctrl "github.com/rhobs/observability-operator/pkg/controllers/monitoring/thanos-querier" + obsuictrl "github.com/rhobs/observability-operator/pkg/controllers/observability-ui/observability-ui-plugin" ) // NOTE: The instance selector label is hardcoded in static assets. @@ -26,12 +29,13 @@ type Operator struct { } type OperatorConfiguration struct { - MetricsAddr string - HealthProbeAddr string - Prometheus stackctrl.PrometheusConfiguration - Alertmanager stackctrl.AlertmanagerConfiguration - ThanosSidecar stackctrl.ThanosConfiguration - ThanosQuerier tqctrl.ThanosConfiguration + MetricsAddr string + HealthProbeAddr string + Prometheus stackctrl.PrometheusConfiguration + Alertmanager stackctrl.AlertmanagerConfiguration + ThanosSidecar stackctrl.ThanosConfiguration + ThanosQuerier tqctrl.ThanosConfiguration + ObservabilityUIPlugins obsuictrl.ObservabilityUIPluginsConfiguration } func WithPrometheusImage(image string) func(*OperatorConfiguration) { @@ -70,6 +74,12 @@ func WithHealthProbeAddr(addr string) func(*OperatorConfiguration) { } } +func WithUIPluginImages(images map[string]string) func(*OperatorConfiguration) { + return func(oc *OperatorConfiguration) { + oc.ObservabilityUIPlugins.Images = images + } +} + func NewOperatorConfiguration(opts ...func(*OperatorConfiguration)) *OperatorConfiguration { cfg := &OperatorConfiguration{} for _, o := range opts { @@ -80,7 +90,7 @@ func NewOperatorConfiguration(opts ...func(*OperatorConfiguration)) *OperatorCon func New(cfg *OperatorConfiguration) (*Operator, error) { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: NewScheme(), + Scheme: runtime.NewScheme(), Metrics: metricsserver.Options{ BindAddress: cfg.MetricsAddr, }, @@ -103,6 +113,10 @@ func New(cfg *OperatorConfiguration) (*Operator, error) { return nil, fmt.Errorf("unable to register the thanos querier controller with the manager: %w", err) } + if err := obsuictrl.RegisterWithManager(mgr, obsuictrl.Options{PluginsConf: cfg.ObservabilityUIPlugins}); err != nil { + return nil, fmt.Errorf("unable to register observability-ui-plugin controller: %w", err) + } + if err := mgr.AddHealthzCheck("health probe", healthz.Ping); err != nil { return nil, fmt.Errorf("unable to add health probe: %w", err) } diff --git a/pkg/operator/scheme.go b/pkg/operator/scheme.go index 24c580242..a66cba400 100644 --- a/pkg/operator/scheme.go +++ b/pkg/operator/scheme.go @@ -1,13 +1,17 @@ package operator import ( - monitoringv1 "github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + osv1alpha1 "github.com/openshift/api/console/v1alpha1" + operatorv1 "github.com/openshift/api/operator/v1" + + monitoringv1 "github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring/v1" rhobsv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1" + rhobsuiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" ) func NewScheme() *runtime.Scheme { @@ -17,6 +21,9 @@ func NewScheme() *runtime.Scheme { utilruntime.Must(rhobsv1alpha1.AddToScheme(scheme)) utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) + utilruntime.Must(rhobsuiv1alpha1.AddToScheme(scheme)) + utilruntime.Must(osv1alpha1.AddToScheme(scheme)) + utilruntime.Must(operatorv1.AddToScheme(scheme)) return scheme } diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index a413c3a65..9d4662347 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -67,6 +67,23 @@ func NewDeleter(r client.Object) Deleter { return Deleter{resource: r} } +type Merger struct { + resource client.Object +} + +func NewMerger(r client.Object) Merger { + return Merger{resource: r} +} + +func (r Merger) Reconcile(ctx context.Context, c client.Client, scheme *runtime.Scheme) error { + if err := c.Patch(ctx, r.resource, client.Merge); err != nil { + return fmt.Errorf("%s/%s (%s): merger failed to patch: %w", + r.resource.GetNamespace(), r.resource.GetName(), + r.resource.GetObjectKind().GroupVersionKind().String(), err) + } + return nil +} + // NewOptionalUpdater ensures that a resource is present or absent depending on the `cond` value (true: present). func NewOptionalUpdater(r client.Object, c metav1.Object, cond bool) Reconciler { if cond { From 730c85c9e3dfcf76819fbcd9efb94c2aede1dd54 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Thu, 14 Mar 2024 10:29:04 +0100 Subject: [PATCH 2/4] feat: add feature gates to enable openshift specific apis and controllers --- cmd/operator/main.go | 14 ++++++++++--- ...bilityui.rhobs_observabilityuiplugins.yaml | 2 +- docs/api.md | 2 +- docs/user-guides/observability-ui-plugins.md | 2 +- pkg/apis/observability-ui/v1alpha1/types.go | 4 ++-- .../compatibility_matrix.go | 11 +++++----- .../compatibility_matrix_test.go | 16 ++++++++++++-- .../observability-ui-plugin/components.go | 5 +++-- .../observability-ui-plugin/controller.go | 16 ++++++++++++++ .../plugin_info_builder.go | 8 +++---- pkg/operator/operator.go | 21 +++++++++++++++++-- pkg/operator/scheme.go | 9 +++++--- test/e2e/main_test.go | 2 +- 13 files changed, 85 insertions(+), 27 deletions(-) diff --git a/cmd/operator/main.go b/cmd/operator/main.go index ccff01947..ef2245e32 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -73,9 +73,10 @@ func validateImages(images *k8sflag.MapStringString) (map[string]string, error) func main() { var ( - namespace string - metricsAddr string - healthProbeAddr string + namespace string + metricsAddr string + healthProbeAddr string + openShiftEnabled bool setupLog = ctrl.Log.WithName("setup") ) @@ -85,6 +86,8 @@ func main() { flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&healthProbeAddr, "health-probe-bind-address", ":8081", "The address the health probe endpoint binds to.") flag.Var(images, "images", fmt.Sprintf("Full images refs to use for containers managed by the operator. E.g thanos=quay.io/thanos/thanos:v0.33.0. Images used are %v", imagesUsed())) + flag.BoolVar(&openShiftEnabled, "openshift.enabled", true, "Enable OpenShift specific features such as Console Plugins.") + opts := zap.Options{ Development: true, TimeEncoder: zapcore.RFC3339TimeEncoder, @@ -114,6 +117,11 @@ func main() { operator.WithThanosSidecarImage(imgMap["thanos"]), operator.WithThanosQuerierImage(imgMap["thanos"]), operator.WithUIPluginImages(imgMap), + operator.WithFeatureGates(operator.FeatureGates{ + OpenShift: operator.OpenShiftFeatureGates{ + Enabled: openShiftEnabled, + }, + }), )) if err != nil { setupLog.Error(err, "cannot create a new operator") diff --git a/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml b/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml index ad81b49ba..05d7148af 100644 --- a/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml +++ b/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml @@ -38,7 +38,7 @@ spec: properties: type: enum: - - dashboards + - Dashboards type: string required: - type diff --git a/docs/api.md b/docs/api.md index f422d05e9..1dd45b5a2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2853,7 +2853,7 @@ Observabilityuipluginpec is the specification for desired state of Observability

- Enum: dashboards
+ Enum: Dashboards
true diff --git a/docs/user-guides/observability-ui-plugins.md b/docs/user-guides/observability-ui-plugins.md index 6cef19c92..f1098a3d9 100644 --- a/docs/user-guides/observability-ui-plugins.md +++ b/docs/user-guides/observability-ui-plugins.md @@ -4,7 +4,7 @@ Using the Observability UI, you can install and manage plugins that extend the o ## Plugins -- [dashboards](#dashboards): Add enhanced dashboards to the OpenShift web console. This plugin allows you to add other prometheus datasources present in the cluster, apart from the in-cluster one, to the default dashboards. +- [dashboards](#dashboards): Add enhanced dashboards to the OpenShift web console. This plugin allows you to add other Prometheus datasources present in the cluster, apart from the in-cluster one, to the default dashboards. ### Dashboards diff --git a/pkg/apis/observability-ui/v1alpha1/types.go b/pkg/apis/observability-ui/v1alpha1/types.go index 7b555c4d5..aac0c0240 100644 --- a/pkg/apis/observability-ui/v1alpha1/types.go +++ b/pkg/apis/observability-ui/v1alpha1/types.go @@ -31,17 +31,17 @@ type ObservabilityUIPluginList struct { Items []ObservabilityUIPlugin `json:"items"` } +// +kubebuilder:validation:Enum=Dashboards type UIPluginType string const ( - TypeDashboards UIPluginType = "dashboards" + TypeDashboards UIPluginType = "Dashboards" ) // Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin. type ObservabilityUIPluginSpec struct { // +required // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=dashboards Type UIPluginType `json:"type"` } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go index 4a09746ef..64f1a57f8 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go +++ b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go @@ -1,6 +1,7 @@ package observability_ui_plugin import ( + "fmt" "strings" "golang.org/x/mod/semver" @@ -26,27 +27,27 @@ var compatibilityMatrix = []CompatibilityEntry{ }, } -func getImageKeyForPluginType(pluginType obsui.UIPluginType, clusterVersion string) string { +func getImageKeyForPluginType(pluginType obsui.UIPluginType, clusterVersion string) (string, error) { if !strings.HasPrefix(clusterVersion, "v") { clusterVersion = "v" + clusterVersion } // No console plugins are supported before 4.11 if semver.Compare(clusterVersion, "v4.11") < 0 { - return "" + return "", fmt.Errorf("dynamic pluings not supported before 4.11") } for _, entry := range compatibilityMatrix { if entry.PluginType == pluginType { if entry.MaxClusterVersion == "" && semver.Compare(clusterVersion, entry.MinClusterVersion) >= 0 { - return entry.ImageKey + return entry.ImageKey, nil } if semver.Compare(clusterVersion, entry.MinClusterVersion) >= 0 && semver.Compare(clusterVersion, entry.MaxClusterVersion) < 0 { - return entry.ImageKey + return entry.ImageKey, nil } } } - return "" + return "", fmt.Errorf("no compatible image found for plugin type %s and cluster version %s", pluginType, clusterVersion) } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go index 253e4842f..ae7d968b1 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go +++ b/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go @@ -1,6 +1,7 @@ package observability_ui_plugin import ( + "fmt" "testing" obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" @@ -13,31 +14,42 @@ func TestCompatibilityMatrixSpec(t *testing.T) { pluginType obsui.UIPluginType clusterVersion string expectedKey string + expectedErr error }{ { pluginType: obsui.TypeDashboards, clusterVersion: "4.10", expectedKey: "", + expectedErr: fmt.Errorf("dynamic pluings not supported before 4.11"), }, { pluginType: obsui.TypeDashboards, clusterVersion: "4.11", expectedKey: "ui-dashboards", + expectedErr: nil, }, { pluginType: obsui.TypeDashboards, clusterVersion: "4.24.0-0.nightly-2024-03-11-200348", expectedKey: "ui-dashboards", + expectedErr: nil, }, { pluginType: "non-existent-plugin", clusterVersion: "4.24.0-0.nightly-2024-03-11-200348", expectedKey: "", + expectedErr: fmt.Errorf("no compatible image found for plugin type non-existent-plugin and cluster version v4.24.0-0.nightly-2024-03-11-200348"), }, } for _, tc := range tt { - actualKey := getImageKeyForPluginType(tc.pluginType, tc.clusterVersion) - assert.DeepEqual(t, tc.expectedKey, actualKey) + actualKey, err := getImageKeyForPluginType(tc.pluginType, tc.clusterVersion) + assert.Equal(t, tc.expectedKey, actualKey) + + if tc.expectedErr != nil { + assert.Error(t, err, tc.expectedErr.Error()) + } else { + assert.NilError(t, err) + } } } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/components.go b/pkg/controllers/observability-ui/observability-ui-plugin/components.go index adef8916e..e96288c1d 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/components.go +++ b/pkg/controllers/observability-ui/observability-ui-plugin/components.go @@ -13,6 +13,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) func pluginComponentReconcilers(plugin *obsui.ObservabilityUIPlugin, pluginInfo ObservabilityUIPluginInfo) []reconciler.Reconciler { @@ -85,7 +86,7 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep Labels: componentLabels(info.Name), }, Spec: appsv1.DeploymentSpec{ - Replicas: func(i int32) *int32 { return &i }(1), + Replicas: ptr.To(int32(1)), Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app.kubernetes.io/instance": info.Name, @@ -149,7 +150,7 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep }, }, }, - ProgressDeadlineSeconds: func(i int32) *int32 { return &i }(300), + ProgressDeadlineSeconds: ptr.To(int32(300)), }, } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/controller.go b/pkg/controllers/observability-ui/observability-ui-plugin/controller.go index 7de3e589f..cbd9e04dc 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/controller.go +++ b/pkg/controllers/observability-ui/observability-ui-plugin/controller.go @@ -12,8 +12,10 @@ import ( v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metaerrors "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -65,6 +67,7 @@ func RegisterWithManager(mgr ctrl.Manager, opts Options) error { if err != nil { logger.Error(err, "failed to get cluster version") + return err } rm := &resourceManager{ @@ -103,8 +106,21 @@ func getClusterVersion(k8client client.Reader) (*configv1.ClusterVersion, error) return clusterVersion, nil } +func (rm resourceManager) consolePluginCapabilityEnabled(ctx context.Context, name types.NamespacedName) bool { + current := &osv1alpha1.ConsolePlugin{} + err := rm.k8sClient.Get(ctx, name, current) + + return err == nil || !metaerrors.IsNoMatchError(err) +} + func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := rm.logger.WithValues("plugin", req.NamespacedName) + + if !rm.consolePluginCapabilityEnabled(ctx, req.NamespacedName) { + logger.Info("Cluster console plugin not supported. Skipping observability UI plugin reconciliation") + return ctrl.Result{}, nil + } + logger.Info("Reconciling observability UI plugin") plugin, err := rm.getUIPlugin(ctx, req) diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go b/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go index dfc5db61e..480153f5c 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go +++ b/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go @@ -21,10 +21,10 @@ type ObservabilityUIPluginInfo struct { } func PluginInfoBuilder(plugin *obsui.ObservabilityUIPlugin, pluginConf ObservabilityUIPluginsConfiguration, clusterVersion string) (*ObservabilityUIPluginInfo, error) { - imageKey := getImageKeyForPluginType(plugin.Spec.Type, clusterVersion) + imageKey, err := getImageKeyForPluginType(plugin.Spec.Type, clusterVersion) - if imageKey == "" { - return nil, fmt.Errorf("no compatible image found for plugin type %s and cluster version %s", plugin.Spec.Type, clusterVersion) + if err != nil { + return nil, err } image := pluginConf.Images[imageKey] @@ -36,7 +36,7 @@ func PluginInfoBuilder(plugin *obsui.ObservabilityUIPlugin, pluginConf Observabi name := "observability-ui-" + plugin.Name switch plugin.Spec.Type { - case "dashboards": + case obsui.TypeDashboards: { readerRoleName := plugin.Name + "-datasource-reader" diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index b870dc4c9..efda3a9e6 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -28,6 +28,14 @@ type Operator struct { manager manager.Manager } +type OpenShiftFeatureGates struct { + Enabled bool `json:"enabled,omitempty"` +} + +type FeatureGates struct { + OpenShift OpenShiftFeatureGates `json:"openshift,omitempty"` +} + type OperatorConfiguration struct { MetricsAddr string HealthProbeAddr string @@ -36,6 +44,7 @@ type OperatorConfiguration struct { ThanosSidecar stackctrl.ThanosConfiguration ThanosQuerier tqctrl.ThanosConfiguration ObservabilityUIPlugins obsuictrl.ObservabilityUIPluginsConfiguration + FeatureGates FeatureGates } func WithPrometheusImage(image string) func(*OperatorConfiguration) { @@ -80,6 +89,12 @@ func WithUIPluginImages(images map[string]string) func(*OperatorConfiguration) { } } +func WithFeatureGates(featureGates FeatureGates) func(*OperatorConfiguration) { + return func(oc *OperatorConfiguration) { + oc.FeatureGates = featureGates + } +} + func NewOperatorConfiguration(opts ...func(*OperatorConfiguration)) *OperatorConfiguration { cfg := &OperatorConfiguration{} for _, o := range opts { @@ -113,8 +128,10 @@ func New(cfg *OperatorConfiguration) (*Operator, error) { return nil, fmt.Errorf("unable to register the thanos querier controller with the manager: %w", err) } - if err := obsuictrl.RegisterWithManager(mgr, obsuictrl.Options{PluginsConf: cfg.ObservabilityUIPlugins}); err != nil { - return nil, fmt.Errorf("unable to register observability-ui-plugin controller: %w", err) + if cfg.FeatureGates.OpenShift.Enabled { + if err := obsuictrl.RegisterWithManager(mgr, obsuictrl.Options{PluginsConf: cfg.ObservabilityUIPlugins}); err != nil { + return nil, fmt.Errorf("unable to register observability-ui-plugin controller: %w", err) + } } if err := mgr.AddHealthzCheck("health probe", healthz.Ping); err != nil { diff --git a/pkg/operator/scheme.go b/pkg/operator/scheme.go index a66cba400..1e70b6c11 100644 --- a/pkg/operator/scheme.go +++ b/pkg/operator/scheme.go @@ -14,7 +14,7 @@ import ( rhobsuiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" ) -func NewScheme() *runtime.Scheme { +func NewScheme(cfg OperatorConfiguration) *runtime.Scheme { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) @@ -22,8 +22,11 @@ func NewScheme() *runtime.Scheme { utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) utilruntime.Must(rhobsuiv1alpha1.AddToScheme(scheme)) - utilruntime.Must(osv1alpha1.AddToScheme(scheme)) - utilruntime.Must(operatorv1.AddToScheme(scheme)) + + if cfg.FeatureGates.OpenShift.Enabled { + utilruntime.Must(osv1alpha1.AddToScheme(scheme)) + utilruntime.Must(operatorv1.AddToScheme(scheme)) + } return scheme } diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 2c005d669..b1a5a8c44 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -84,7 +84,7 @@ func main(m *testing.M) int { func setupFramework() error { cfg := config.GetConfigOrDie() k8sClient, err := client.New(cfg, client.Options{ - Scheme: operator.NewScheme(), + Scheme: operator.NewScheme(operator.OperatorConfiguration{}), }) if err != nil { return err From bc3b3a772688966132206091bf7b41bcbe5fa0b5 Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Tue, 16 Apr 2024 09:26:58 +0200 Subject: [PATCH 3/4] feat: rename ui plugins group name --- ...bility-operator.clusterserviceversion.yaml | 22 +-- .../observability.openshift.io_uiplugins.yaml | 126 ++++++++++++++++ ...bilityui.rhobs_observabilityuiplugins.yaml | 122 --------------- deploy/crds/common/kustomization.yaml | 2 +- .../observability.openshift.io_uiplugins.yaml | 120 +++++++++++++++ ...bilityui.rhobs_observabilityuiplugins.yaml | 117 --------------- ...bility-operator.clusterserviceversion.yaml | 8 +- deploy/operator/kustomization.yaml | 8 + .../observability-operator-cluster-role.yaml | 14 +- docs/api.md | 63 ++++---- docs/user-guides/observability-ui-plugins.md | 10 +- .../v1alpha1/register.go | 6 +- .../v1alpha1/types.go | 30 ++-- .../v1alpha1/zz_generated.deepcopy.go | 39 +++-- .../compatibility_matrix.go | 10 +- .../compatibility_matrix_test.go | 14 +- .../components.go | 55 +++---- .../controller.go | 139 +++++++++++++----- .../plugin_info_builder.go | 15 +- pkg/operator/operator.go | 26 ++-- pkg/operator/scheme.go | 13 +- test/e2e/main_test.go | 2 +- 22 files changed, 525 insertions(+), 436 deletions(-) create mode 100644 bundle/manifests/observability.openshift.io_uiplugins.yaml delete mode 100644 bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml create mode 100644 deploy/crds/common/observability.openshift.io_uiplugins.yaml delete mode 100644 deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml rename pkg/apis/{observability-ui => uiplugin}/v1alpha1/register.go (84%) rename pkg/apis/{observability-ui => uiplugin}/v1alpha1/types.go (80%) rename pkg/apis/{observability-ui => uiplugin}/v1alpha1/zz_generated.deepcopy.go (71%) rename pkg/controllers/{observability-ui/observability-ui-plugin => uiplugin}/compatibility_matrix.go (79%) rename pkg/controllers/{observability-ui/observability-ui-plugin => uiplugin}/compatibility_matrix_test.go (80%) rename pkg/controllers/{observability-ui/observability-ui-plugin => uiplugin}/components.go (76%) rename pkg/controllers/{observability-ui/observability-ui-plugin => uiplugin}/controller.go (61%) rename pkg/controllers/{observability-ui/observability-ui-plugin => uiplugin}/plugin_info_builder.go (86%) diff --git a/bundle/manifests/observability-operator.clusterserviceversion.yaml b/bundle/manifests/observability-operator.clusterserviceversion.yaml index a008f50e7..8b17bb852 100644 --- a/bundle/manifests/observability-operator.clusterserviceversion.yaml +++ b/bundle/manifests/observability-operator.clusterserviceversion.yaml @@ -75,9 +75,6 @@ spec: kind: MonitoringStack name: monitoringstacks.monitoring.rhobs version: v1alpha1 - - kind: ObservabilityUIPlugin - name: observabilityuiplugins.observabilityui.rhobs - version: v1alpha1 - description: PodMonitor defines monitoring for a set of pods displayName: PodMonitor kind: PodMonitor @@ -126,6 +123,11 @@ spec: kind: ThanosRuler name: thanosrulers.monitoring.rhobs version: v1 + - description: UIPlugin defines a console plugin for observability + displayName: UIPlugin + kind: UIPlugin + name: uiplugins.observability.openshift.io + version: v1alpha1 description: |2+ Observability Operator is a Go based Kubernetes operator to setup and manage highly available Monitoring stack using Prometheus, Alertmanager and Thanos Querier. @@ -429,9 +431,9 @@ spec: - patch - update - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins + - uiplugins verbs: - create - delete @@ -441,17 +443,17 @@ spec: - update - watch - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins/finalizers - - observabilityuiplugins/status + - uiplugins/finalizers + - uiplugins/status verbs: - get - update - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins/status + - uiplugins/status verbs: - get - update diff --git a/bundle/manifests/observability.openshift.io_uiplugins.yaml b/bundle/manifests/observability.openshift.io_uiplugins.yaml new file mode 100644 index 000000000..1c30bb9c3 --- /dev/null +++ b/bundle/manifests/observability.openshift.io_uiplugins.yaml @@ -0,0 +1,126 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + creationTimestamp: null + name: uiplugins.observability.openshift.io +spec: + group: observability.openshift.io + names: + kind: UIPlugin + listKind: UIPluginList + plural: uiplugins + singular: uiplugin + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: UIPlugin defines an observability console plugin + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: UIPluginSpec is the specification for desired state of UIPlugin. + properties: + type: + enum: + - Dashboards + type: string + required: + - type + type: object + status: + description: |- + UIPluginStatus defines the observed state of UIPlugin. + It should always be reconstructable from the state of the cluster and/or outside world. + properties: + conditions: + description: Conditions provide status information about the plugin. + items: + 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 + enum: + - "True" + - "False" + - Unknown + - Degraded + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + 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 + x-kubernetes-list-type: atomic + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml b/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml deleted file mode 100644 index 6d105f303..000000000 --- a/bundle/manifests/observabilityui.rhobs_observabilityuiplugins.yaml +++ /dev/null @@ -1,122 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null - name: observabilityuiplugins.observabilityui.rhobs -spec: - group: observabilityui.rhobs - names: - kind: ObservabilityUIPlugin - listKind: ObservabilityUIPluginList - plural: observabilityuiplugins - singular: observabilityuiplugin - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: ObservabilityUIPlugin defines an observability console plugin - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Observabilityuipluginpec is the specification for desired - state of ObservabilityUIPlugin. - properties: - type: - enum: - - dashboards - type: string - required: - - type - type: object - status: - description: Observabilityuiplugintatus defines the observed state of - ObservabilityUIPlugin. It should always be reconstructable from the - state of the cluster and/or outside world. - properties: - conditions: - description: Conditions provide status information about the plugin. - items: - 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 - enum: - - "True" - - "False" - - Unknown - - Degraded - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - 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 - x-kubernetes-list-type: atomic - required: - - conditions - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null diff --git a/deploy/crds/common/kustomization.yaml b/deploy/crds/common/kustomization.yaml index f4b7aab02..66ca371f8 100644 --- a/deploy/crds/common/kustomization.yaml +++ b/deploy/crds/common/kustomization.yaml @@ -4,4 +4,4 @@ kind: Kustomization resources: - monitoring.rhobs_monitoringstacks.yaml - monitoring.rhobs_thanosqueriers.yaml - - observabilityui.rhobs_observabilityuiplugins.yaml + - observability.openshift.io_uiplugins.yaml diff --git a/deploy/crds/common/observability.openshift.io_uiplugins.yaml b/deploy/crds/common/observability.openshift.io_uiplugins.yaml new file mode 100644 index 000000000..ccabd2cf2 --- /dev/null +++ b/deploy/crds/common/observability.openshift.io_uiplugins.yaml @@ -0,0 +1,120 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: uiplugins.observability.openshift.io +spec: + group: observability.openshift.io + names: + kind: UIPlugin + listKind: UIPluginList + plural: uiplugins + singular: uiplugin + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: UIPlugin defines an observability console plugin + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: UIPluginSpec is the specification for desired state of UIPlugin. + properties: + type: + enum: + - Dashboards + type: string + required: + - type + type: object + status: + description: |- + UIPluginStatus defines the observed state of UIPlugin. + It should always be reconstructable from the state of the cluster and/or outside world. + properties: + conditions: + description: Conditions provide status information about the plugin. + items: + 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 + enum: + - "True" + - "False" + - Unknown + - Degraded + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + 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 + x-kubernetes-list-type: atomic + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml b/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml deleted file mode 100644 index 05d7148af..000000000 --- a/deploy/crds/common/observabilityui.rhobs_observabilityuiplugins.yaml +++ /dev/null @@ -1,117 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null - name: observabilityuiplugins.observabilityui.rhobs -spec: - group: observabilityui.rhobs - names: - kind: ObservabilityUIPlugin - listKind: ObservabilityUIPluginList - plural: observabilityuiplugins - singular: observabilityuiplugin - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: ObservabilityUIPlugin defines an observability console plugin - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Observabilityuipluginpec is the specification for desired - state of ObservabilityUIPlugin. - properties: - type: - enum: - - Dashboards - type: string - required: - - type - type: object - status: - description: Observabilityuiplugintatus defines the observed state of - ObservabilityUIPlugin. It should always be reconstructable from the - state of the cluster and/or outside world. - properties: - conditions: - description: Conditions provide status information about the plugin. - items: - 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 - enum: - - "True" - - "False" - - Unknown - - Degraded - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - 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 - x-kubernetes-list-type: atomic - required: - - conditions - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/deploy/olm/bases/observability-operator.clusterserviceversion.yaml b/deploy/olm/bases/observability-operator.clusterserviceversion.yaml index d61f42599..0faed8759 100644 --- a/deploy/olm/bases/observability-operator.clusterserviceversion.yaml +++ b/deploy/olm/bases/observability-operator.clusterserviceversion.yaml @@ -85,10 +85,10 @@ spec: kind: ThanosRuler name: thanosrulers.monitoring.rhobs version: v1 - - description: ObservabilityUIPlugin defines a console plugin for observability - displayName: ObservabilityUIPlugin - kind: ObservabilityUIPlugin - name: observabilityui.rhobs + - description: UIPlugin defines a console plugin for observability + displayName: UIPlugin + kind: UIPlugin + name: uiplugins.observability.openshift.io version: v1alpha1 description: >+ diff --git a/deploy/operator/kustomization.yaml b/deploy/operator/kustomization.yaml index fa5ebc19d..496b3b0b7 100644 --- a/deploy/operator/kustomization.yaml +++ b/deploy/operator/kustomization.yaml @@ -38,6 +38,14 @@ patches: group: apps kind: Deployment version: v1 +- patch: |- + - op: add + path: /spec/template/spec/containers/0/args/- + value: --images=ui-dashboards=quay.io/openshift-observability-ui/console-dashboards-plugin:v0.1.0 + target: + group: apps + kind: Deployment + version: v1 - patch: |- - op: add path: /spec/template/spec/containers/0/resources diff --git a/deploy/operator/observability-operator-cluster-role.yaml b/deploy/operator/observability-operator-cluster-role.yaml index b395cbc4e..5c5fabc6e 100644 --- a/deploy/operator/observability-operator-cluster-role.yaml +++ b/deploy/operator/observability-operator-cluster-role.yaml @@ -168,9 +168,9 @@ rules: - patch - update - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins + - uiplugins verbs: - create - delete @@ -180,17 +180,17 @@ rules: - update - watch - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins/finalizers - - observabilityuiplugins/status + - uiplugins/finalizers + - uiplugins/status verbs: - get - update - apiGroups: - - observabilityui.rhobs + - observability.openshift.io resources: - - observabilityuiplugins/status + - uiplugins/status verbs: - get - update diff --git a/docs/api.md b/docs/api.md index 1dd45b5a2..b01d8402f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,7 +3,7 @@ Packages: - [monitoring.rhobs/v1alpha1](#monitoringrhobsv1alpha1) -- [observabilityui.rhobs/v1alpha1](#observabilityuirhobsv1alpha1) +- [observability.openshift.io/v1alpha1](#observabilityopenshiftiov1alpha1) # monitoring.rhobs/v1alpha1 @@ -2768,24 +2768,24 @@ list restricting them.
-# observabilityui.rhobs/v1alpha1 +# observability.openshift.io/v1alpha1 Resource Types: -- [ObservabilityUIPlugin](#observabilityuiplugin) +- [UIPlugin](#uiplugin) -## ObservabilityUIPlugin -[↩ Parent](#observabilityuirhobsv1alpha1 ) +## UIPlugin +[↩ Parent](#observabilityopenshiftiov1alpha1 ) -ObservabilityUIPlugin defines an observability console plugin +UIPlugin defines an observability console plugin @@ -2799,13 +2799,13 @@ ObservabilityUIPlugin defines an observability console plugin - + - + @@ -2814,29 +2814,30 @@ ObservabilityUIPlugin defines an observability console plugin - + - +
apiVersion stringobservabilityui.rhobs/v1alpha1observability.openshift.io/v1alpha1 true
kind stringObservabilityUIPluginUIPlugin true
Refer to the Kubernetes API documentation for the fields of the `metadata` field. true
specspec object - Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin.
+ UIPluginSpec is the specification for desired state of UIPlugin.
false
statusstatus object - Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. It should always be reconstructable from the state of the cluster and/or outside world.
+ UIPluginStatus defines the observed state of UIPlugin. +It should always be reconstructable from the state of the cluster and/or outside world.
false
-### ObservabilityUIPlugin.spec -[↩ Parent](#observabilityuiplugin) +### UIPlugin.spec +[↩ Parent](#uiplugin) -Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin. +UIPluginSpec is the specification for desired state of UIPlugin. @@ -2860,12 +2861,13 @@ Observabilityuipluginpec is the specification for desired state of Observability
-### ObservabilityUIPlugin.status -[↩ Parent](#observabilityuiplugin) +### UIPlugin.status +[↩ Parent](#uiplugin) -Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. It should always be reconstructable from the state of the cluster and/or outside world. +UIPluginStatus defines the observed state of UIPlugin. +It should always be reconstructable from the state of the cluster and/or outside world. @@ -2877,7 +2879,7 @@ Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. - +
conditionsconditions []object Conditions provide status information about the plugin.
@@ -2887,8 +2889,8 @@ Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin.
-### ObservabilityUIPlugin.status.conditions[index] -[↩ Parent](#observabilityuipluginstatus) +### UIPlugin.status.conditions[index] +[↩ Parent](#uipluginstatus) @@ -2907,7 +2909,8 @@ Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. lastTransitionTime string - 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.
+ 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
@@ -2916,14 +2919,19 @@ Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. message string - message is a human readable message indicating details about the transition. This may be an empty string.
+ message is a human readable message indicating details about the transition. +This may be an empty string.
true reason string - 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.
+ 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.
true @@ -2939,14 +2947,17 @@ Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. type string - type of condition in CamelCase or in foo.example.com/CamelCase. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ type of condition in CamelCase or in foo.example.com/CamelCase. +The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
true observedGeneration integer - 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.
+ 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
diff --git a/docs/user-guides/observability-ui-plugins.md b/docs/user-guides/observability-ui-plugins.md index f1098a3d9..be04e7440 100644 --- a/docs/user-guides/observability-ui-plugins.md +++ b/docs/user-guides/observability-ui-plugins.md @@ -8,16 +8,16 @@ Using the Observability UI, you can install and manage plugins that extend the o ### Dashboards -The plugin will search for datasources as ConfigMaps in the `console-dashboards` namespace with the `console.openshift.io/dashboard-datasource: 'true'` label. The namespace `console-dashboards` is required, more details on how to create a datasource ConfigMap can be found in the [console-dashboards-plugin](https://github.com/openshift/console-dashboards-plugin/blob/main/docs/add-datasource.md) +The plugin will search for datasources as ConfigMaps in the `openshift-config-managed` namespace with the `console.openshift.io/dashboard-datasource: 'true'` label. The namespace `openshift-config-managed` is required, more details on how to create a datasource ConfigMap can be found in the [console-dashboards-plugin](https://github.com/openshift/console-dashboards-plugin/blob/main/docs/add-datasource.md) -To enable the console dashboards plugin, create a `ObservabilityUIPlugin` CR. The following example shows how to create a CR to enable the console dashboards plugin: +To enable the console dashboards plugin, create a `UIPlugin` CR. The following example shows how to create a CR to enable the console dashboards plugin: ```yaml -apiVersion: observabilityui.rhobs/v1alpha1 -kind: ObservabilityUIPlugin +apiVersion: observability.openshift.io/v1alpha1 +kind: UIPlugin metadata: name: ui-dashboards - namespace: openshift-observability-ui + namespace: observability-ui spec: type: Dashboards ``` diff --git a/pkg/apis/observability-ui/v1alpha1/register.go b/pkg/apis/uiplugin/v1alpha1/register.go similarity index 84% rename from pkg/apis/observability-ui/v1alpha1/register.go rename to pkg/apis/uiplugin/v1alpha1/register.go index 848c78812..2db370307 100644 --- a/pkg/apis/observability-ui/v1alpha1/register.go +++ b/pkg/apis/uiplugin/v1alpha1/register.go @@ -16,7 +16,7 @@ limitations under the License. // Package v1alpha1 contains API Schema definitions for the rhobs v1alpha1 API group // +kubebuilder:object:generate=true -// +groupName=observabilityui.rhobs +// +groupName=observability.openshift.io package v1alpha1 import ( @@ -26,7 +26,7 @@ import ( var ( // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "observabilityui.rhobs", Version: "v1alpha1"} + GroupVersion = schema.GroupVersion{Group: "observability.openshift.io", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} @@ -36,5 +36,5 @@ var ( ) func init() { - SchemeBuilder.Register(&ObservabilityUIPlugin{}, &ObservabilityUIPluginList{}) + SchemeBuilder.Register(&UIPlugin{}, &UIPluginList{}) } diff --git a/pkg/apis/observability-ui/v1alpha1/types.go b/pkg/apis/uiplugin/v1alpha1/types.go similarity index 80% rename from pkg/apis/observability-ui/v1alpha1/types.go rename to pkg/apis/uiplugin/v1alpha1/types.go index aac0c0240..fbc341ee9 100644 --- a/pkg/apis/observability-ui/v1alpha1/types.go +++ b/pkg/apis/uiplugin/v1alpha1/types.go @@ -1,6 +1,6 @@ -// +groupName=observabilityui.rhobs -// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins,verbs=list;get;watch -// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins/status;observabilityuiplugins/finalizers,verbs=get;update +// +groupName=observability.openshift.io +// +kubebuilder:rbac:groups=observability.openshift.io,resources=uiplugins,verbs=list;get;watch +// +kubebuilder:rbac:groups=observability.openshift.io,resources=uiplugins/status;uiplugins/finalizers,verbs=get;update package v1alpha1 @@ -10,44 +10,46 @@ import ( // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// ObservabilityUIPlugin defines an observability console plugin +// UIPlugin defines an observability console plugin. // +k8s:openapi-gen=true // +kubebuilder:resource // +kubebuilder:subresource:status -type ObservabilityUIPlugin struct { +type UIPlugin struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ObservabilityUIPluginSpec `json:"spec,omitempty"` - Status ObservabilityUIPluginStatus `json:"status,omitempty"` + Spec UIPluginSpec `json:"spec,omitempty"` + Status UIPluginStatus `json:"status,omitempty"` } -// ObservabilityUIPluginList contains a list of ObservabilityUIPlugin +// UIPluginList contains a list of UIPlugin // +kubebuilder:resource // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ObservabilityUIPluginList struct { +type UIPluginList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []ObservabilityUIPlugin `json:"items"` + Items []UIPlugin `json:"items"` } // +kubebuilder:validation:Enum=Dashboards type UIPluginType string const ( + // Dashboards deploys the Dashboards Dynamic Plugin for OpenShift Console. TypeDashboards UIPluginType = "Dashboards" ) -// Observabilityuipluginpec is the specification for desired state of ObservabilityUIPlugin. -type ObservabilityUIPluginSpec struct { +// UIPluginSpec is the specification for desired state of UIPlugin. +type UIPluginSpec struct { + // Type defines the UI plugin. // +required // +kubebuilder:validation:Required Type UIPluginType `json:"type"` } -// Observabilityuiplugintatus defines the observed state of ObservabilityUIPlugin. +// UIPluginStatus defines the observed state of UIPlugin. // It should always be reconstructable from the state of the cluster and/or outside world. -type ObservabilityUIPluginStatus struct { +type UIPluginStatus struct { // Conditions provide status information about the plugin. // +listType=atomic Conditions []Condition `json:"conditions"` diff --git a/pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go similarity index 71% rename from pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go rename to pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go index 008ec4d78..292e70e7e 100644 --- a/pkg/apis/observability-ui/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/uiplugin/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2021. @@ -42,7 +41,7 @@ func (in *Condition) DeepCopy() *Condition { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObservabilityUIPlugin) DeepCopyInto(out *ObservabilityUIPlugin) { +func (in *UIPlugin) DeepCopyInto(out *UIPlugin) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -50,18 +49,18 @@ func (in *ObservabilityUIPlugin) DeepCopyInto(out *ObservabilityUIPlugin) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPlugin. -func (in *ObservabilityUIPlugin) DeepCopy() *ObservabilityUIPlugin { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIPlugin. +func (in *UIPlugin) DeepCopy() *UIPlugin { if in == nil { return nil } - out := new(ObservabilityUIPlugin) + out := new(UIPlugin) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ObservabilityUIPlugin) DeepCopyObject() runtime.Object { +func (in *UIPlugin) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -69,31 +68,31 @@ func (in *ObservabilityUIPlugin) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObservabilityUIPluginList) DeepCopyInto(out *ObservabilityUIPluginList) { +func (in *UIPluginList) DeepCopyInto(out *UIPluginList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]ObservabilityUIPlugin, len(*in)) + *out = make([]UIPlugin, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginList. -func (in *ObservabilityUIPluginList) DeepCopy() *ObservabilityUIPluginList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIPluginList. +func (in *UIPluginList) DeepCopy() *UIPluginList { if in == nil { return nil } - out := new(ObservabilityUIPluginList) + out := new(UIPluginList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ObservabilityUIPluginList) DeepCopyObject() runtime.Object { +func (in *UIPluginList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -101,22 +100,22 @@ func (in *ObservabilityUIPluginList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObservabilityUIPluginSpec) DeepCopyInto(out *ObservabilityUIPluginSpec) { +func (in *UIPluginSpec) DeepCopyInto(out *UIPluginSpec) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginSpec. -func (in *ObservabilityUIPluginSpec) DeepCopy() *ObservabilityUIPluginSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIPluginSpec. +func (in *UIPluginSpec) DeepCopy() *UIPluginSpec { if in == nil { return nil } - out := new(ObservabilityUIPluginSpec) + out := new(UIPluginSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ObservabilityUIPluginStatus) DeepCopyInto(out *ObservabilityUIPluginStatus) { +func (in *UIPluginStatus) DeepCopyInto(out *UIPluginStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -127,12 +126,12 @@ func (in *ObservabilityUIPluginStatus) DeepCopyInto(out *ObservabilityUIPluginSt } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityUIPluginStatus. -func (in *ObservabilityUIPluginStatus) DeepCopy() *ObservabilityUIPluginStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIPluginStatus. +func (in *UIPluginStatus) DeepCopy() *UIPluginStatus { if in == nil { return nil } - out := new(ObservabilityUIPluginStatus) + out := new(UIPluginStatus) in.DeepCopyInto(out) return out } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go b/pkg/controllers/uiplugin/compatibility_matrix.go similarity index 79% rename from pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go rename to pkg/controllers/uiplugin/compatibility_matrix.go index 64f1a57f8..6a643a579 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix.go +++ b/pkg/controllers/uiplugin/compatibility_matrix.go @@ -1,4 +1,4 @@ -package observability_ui_plugin +package uiplugin import ( "fmt" @@ -6,11 +6,11 @@ import ( "golang.org/x/mod/semver" - obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) type CompatibilityEntry struct { - PluginType obsui.UIPluginType + PluginType uiv1alpha1.UIPluginType MinClusterVersion string MaxClusterVersion string ImageKey string @@ -19,7 +19,7 @@ type CompatibilityEntry struct { var compatibilityMatrix = []CompatibilityEntry{ { - PluginType: obsui.TypeDashboards, + PluginType: uiv1alpha1.TypeDashboards, MinClusterVersion: "v4.11", MaxClusterVersion: "", ImageKey: "ui-dashboards", @@ -27,7 +27,7 @@ var compatibilityMatrix = []CompatibilityEntry{ }, } -func getImageKeyForPluginType(pluginType obsui.UIPluginType, clusterVersion string) (string, error) { +func getImageKeyForPluginType(pluginType uiv1alpha1.UIPluginType, clusterVersion string) (string, error) { if !strings.HasPrefix(clusterVersion, "v") { clusterVersion = "v" + clusterVersion } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go b/pkg/controllers/uiplugin/compatibility_matrix_test.go similarity index 80% rename from pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go rename to pkg/controllers/uiplugin/compatibility_matrix_test.go index ae7d968b1..4ca81df49 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/compatibility_matrix_test.go +++ b/pkg/controllers/uiplugin/compatibility_matrix_test.go @@ -1,35 +1,35 @@ -package observability_ui_plugin +package uiplugin import ( "fmt" "testing" - obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" - "gotest.tools/v3/assert" + + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) func TestCompatibilityMatrixSpec(t *testing.T) { tt := []struct { - pluginType obsui.UIPluginType + pluginType uiv1alpha1.UIPluginType clusterVersion string expectedKey string expectedErr error }{ { - pluginType: obsui.TypeDashboards, + pluginType: uiv1alpha1.TypeDashboards, clusterVersion: "4.10", expectedKey: "", expectedErr: fmt.Errorf("dynamic pluings not supported before 4.11"), }, { - pluginType: obsui.TypeDashboards, + pluginType: uiv1alpha1.TypeDashboards, clusterVersion: "4.11", expectedKey: "ui-dashboards", expectedErr: nil, }, { - pluginType: obsui.TypeDashboards, + pluginType: uiv1alpha1.TypeDashboards, clusterVersion: "4.24.0-0.nightly-2024-03-11-200348", expectedKey: "ui-dashboards", expectedErr: nil, diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/components.go b/pkg/controllers/uiplugin/components.go similarity index 76% rename from pkg/controllers/observability-ui/observability-ui-plugin/components.go rename to pkg/controllers/uiplugin/components.go index e96288c1d..7a6044962 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/components.go +++ b/pkg/controllers/uiplugin/components.go @@ -1,22 +1,27 @@ -package observability_ui_plugin +package uiplugin import ( "fmt" - "github.com/rhobs/observability-operator/pkg/reconciler" - osv1alpha1 "github.com/openshift/api/console/v1alpha1" - obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" + "github.com/rhobs/observability-operator/pkg/reconciler" +) + +const ( + port = 9443 + serviceAccountSuffix = "-sa" + servingCertVolumeName = "serving-cert" ) -func pluginComponentReconcilers(plugin *obsui.ObservabilityUIPlugin, pluginInfo ObservabilityUIPluginInfo) []reconciler.Reconciler { +func pluginComponentReconcilers(plugin *uiv1alpha1.UIPlugin, pluginInfo UIPluginInfo) []reconciler.Reconciler { hasClusterRole := pluginInfo.ClusterRole != nil hasClusterRoleBinding := pluginInfo.ClusterRoleBinding != nil namespace := plugin.Namespace @@ -31,28 +36,28 @@ func pluginComponentReconcilers(plugin *obsui.ObservabilityUIPlugin, pluginInfo } } -func newServiceAccount(info ObservabilityUIPluginInfo, namespace string) *corev1.ServiceAccount { +func newServiceAccount(info UIPluginInfo, namespace string) *corev1.ServiceAccount { return &corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ServiceAccount", }, ObjectMeta: metav1.ObjectMeta{ - Name: info.Name + "-sa", + Name: info.Name + serviceAccountSuffix, Namespace: namespace, }, } } -func newClusterRole(info ObservabilityUIPluginInfo) *rbacv1.ClusterRole { +func newClusterRole(info UIPluginInfo) *rbacv1.ClusterRole { return info.ClusterRole } -func newClusterRoleBinding(info ObservabilityUIPluginInfo) *rbacv1.ClusterRoleBinding { +func newClusterRoleBinding(info UIPluginInfo) *rbacv1.ClusterRoleBinding { return info.ClusterRoleBinding } -func newConsolePlugin(info ObservabilityUIPluginInfo, namespace string) *osv1alpha1.ConsolePlugin { +func newConsolePlugin(info UIPluginInfo, namespace string) *osv1alpha1.ConsolePlugin { return &osv1alpha1.ConsolePlugin{ TypeMeta: metav1.TypeMeta{ APIVersion: osv1alpha1.SchemeGroupVersion.String(), @@ -66,7 +71,7 @@ func newConsolePlugin(info ObservabilityUIPluginInfo, namespace string) *osv1alp Service: osv1alpha1.ConsolePluginService{ Name: info.Name, Namespace: namespace, - Port: 9443, + Port: port, BasePath: "/", }, Proxy: info.Proxies, @@ -74,7 +79,7 @@ func newConsolePlugin(info ObservabilityUIPluginInfo, namespace string) *osv1alp } } -func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Deployment { +func newDeployment(info UIPluginInfo, namespace string) *appsv1.Deployment { plugin := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ APIVersion: appsv1.SchemeGroupVersion.String(), @@ -99,21 +104,21 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep Labels: componentLabels(info.Name), }, Spec: corev1.PodSpec{ - ServiceAccountName: info.Name + "-sa", + ServiceAccountName: info.Name + serviceAccountSuffix, Containers: []corev1.Container{ { Name: info.Name, Image: info.Image, Ports: []corev1.ContainerPort{ { - ContainerPort: 9443, + ContainerPort: port, Name: "web", }, }, TerminationMessagePolicy: "FallbackToLogsOnError", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &[]bool{true}[0], - AllowPrivilegeEscalation: &[]bool{false}[0], + RunAsNonRoot: ptr.To(bool(true)), + AllowPrivilegeEscalation: ptr.To(bool(false)), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", @@ -122,13 +127,13 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep }, VolumeMounts: []corev1.VolumeMount{ { - Name: "serving-cert", + Name: servingCertVolumeName, ReadOnly: true, MountPath: "/var/serving-cert", }, }, Args: []string{ - fmt.Sprintf("-port=%d", 9443), + fmt.Sprintf("-port=%d", port), "-cert=/var/serving-cert/tls.crt", "-key=/var/serving-cert/tls.key", }, @@ -136,11 +141,11 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep }, Volumes: []corev1.Volume{ { - Name: "serving-cert", + Name: servingCertVolumeName, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: info.Name, - DefaultMode: &[]int32{420}[0], + DefaultMode: ptr.To(int32(420)), }, }, }, @@ -157,7 +162,7 @@ func newDeployment(info ObservabilityUIPluginInfo, namespace string) *appsv1.Dep return plugin } -func newService(info ObservabilityUIPluginInfo, namespace string) *corev1.Service { +func newService(info UIPluginInfo, namespace string) *corev1.Service { annotations := map[string]string{ "service.alpha.openshift.io/serving-cert-secret-name": info.Name, } @@ -176,10 +181,10 @@ func newService(info ObservabilityUIPluginInfo, namespace string) *corev1.Servic Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { - Port: 9443, + Port: port, Name: "http", Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromInt32(9443), + TargetPort: intstr.FromInt32(port), }, }, Selector: map[string]string{ @@ -193,7 +198,7 @@ func newService(info ObservabilityUIPluginInfo, namespace string) *corev1.Servic func componentLabels(pluginName string) map[string]string { return map[string]string{ "app.kubernetes.io/instance": pluginName, - "app.kubernetes.io/part-of": "ObservabilityUIPlugin", + "app.kubernetes.io/part-of": "UIPlugin", "app.kubernetes.io/managed-by": "observability-operator", } } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/controller.go b/pkg/controllers/uiplugin/controller.go similarity index 61% rename from pkg/controllers/observability-ui/observability-ui-plugin/controller.go rename to pkg/controllers/uiplugin/controller.go index cbd9e04dc..1798fbad8 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/controller.go +++ b/pkg/controllers/uiplugin/controller.go @@ -1,4 +1,4 @@ -package observability_ui_plugin +package uiplugin import ( "context" @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" configv1 "github.com/openshift/api/config/v1" osv1alpha1 "github.com/openshift/api/console/v1alpha1" + operatorv1 "github.com/openshift/api/operator/v1" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -18,13 +19,11 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/predicate" - operatorv1 "github.com/openshift/api/operator/v1" - obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" "github.com/rhobs/observability-operator/pkg/reconciler" ) @@ -33,21 +32,29 @@ type resourceManager struct { scheme *runtime.Scheme logger logr.Logger controller controller.Controller - pluginConf ObservabilityUIPluginsConfiguration + pluginConf UIPluginsConfiguration clusterVersion string } -type ObservabilityUIPluginsConfiguration struct { +type UIPluginsConfiguration struct { Images map[string]string } type Options struct { - PluginsConf ObservabilityUIPluginsConfiguration + PluginsConf UIPluginsConfiguration } -// RBAC for managing ObservabilityUIPlugins -// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins,verbs=get;list;watch;create;update;delete;patch -// +kubebuilder:rbac:groups=observabilityui.rhobs,resources=observabilityuiplugins/status,verbs=get;update +const ( + AvailableReason = "UIPluginAvailable" + ReconciledReason = "UIPluginReconciled" + FailedToReconcileReason = "UIPluginFailedToReconciled" + ReconciledMessage = "Plugin reconciled successfully" + NoReason = "None" +) + +// RBAC for managing UIPlugins +// +kubebuilder:rbac:groups=observability.openshift.io,resources=uiplugins,verbs=get;list;watch;create;update;delete;patch +// +kubebuilder:rbac:groups=observability.openshift.io,resources=uiplugins/status,verbs=get;update // RBAC for managing observability ui plugin objects //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;update;patch;delete @@ -81,7 +88,7 @@ func RegisterWithManager(mgr ctrl.Manager, opts Options) error { generationChanged := builder.WithPredicates(predicate.GenerationChangedPredicate{}) ctrl, err := ctrl.NewControllerManagedBy(mgr). - For(&obsui.ObservabilityUIPlugin{}). + For(&uiv1alpha1.UIPlugin{}). Owns(&appsv1.Deployment{}, generationChanged). Owns(&v1.Service{}, generationChanged). Owns(&v1.ServiceAccount{}, generationChanged). @@ -117,7 +124,7 @@ func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl logger := rm.logger.WithValues("plugin", req.NamespacedName) if !rm.consolePluginCapabilityEnabled(ctx, req.NamespacedName) { - logger.Info("Cluster console plugin not supported. Skipping observability UI plugin reconciliation") + logger.Info("Cluster console plugin not supported or not accessible. Skipping observability UI plugin reconciliation") return ctrl.Result{}, nil } @@ -156,62 +163,114 @@ func (rm resourceManager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{RequeueAfter: 2 * time.Second}, nil } if err != nil { - return ctrl.Result{}, err + return rm.updateStatus(ctx, req, plugin, err), err } } if err := rm.registerPluginWithConsole(ctx, pluginInfo); err != nil { - return ctrl.Result{}, err + return rm.updateStatus(ctx, req, plugin, err), err } - return ctrl.Result{}, nil + return rm.updateStatus(ctx, req, plugin, nil), nil } -func (rm resourceManager) registerPluginWithConsole(ctx context.Context, pluginInfo *ObservabilityUIPluginInfo) error { +func (rm resourceManager) updateStatus(ctx context.Context, req ctrl.Request, pl *uiv1alpha1.UIPlugin, recError error) ctrl.Result { + logger := rm.logger.WithValues("plugin", req.NamespacedName) + + if recError != nil { + pl.Status.Conditions = []uiv1alpha1.Condition{ + { + Type: uiv1alpha1.ReconciledCondition, + Status: uiv1alpha1.ConditionFalse, + Reason: FailedToReconcileReason, + Message: recError.Error(), + ObservedGeneration: pl.Generation, + LastTransitionTime: metav1.Now(), + }, + { + Type: uiv1alpha1.AvailableCondition, + Status: uiv1alpha1.ConditionFalse, + Reason: FailedToReconcileReason, + ObservedGeneration: pl.Generation, + LastTransitionTime: metav1.Now(), + }, + } + } else { + pl.Status.Conditions = []uiv1alpha1.Condition{ + { + Type: uiv1alpha1.ReconciledCondition, + Status: uiv1alpha1.ConditionTrue, + Reason: ReconciledReason, + Message: ReconciledMessage, + ObservedGeneration: pl.Generation, + LastTransitionTime: metav1.Now(), + }, + { + Type: uiv1alpha1.AvailableCondition, + Status: uiv1alpha1.ConditionTrue, + Reason: AvailableReason, + ObservedGeneration: pl.Generation, + LastTransitionTime: metav1.Now(), + }, + } + } + + err := rm.k8sClient.Status().Update(ctx, pl) + if err != nil { + logger.Info("Failed to update status", "err", err) + return ctrl.Result{RequeueAfter: 2 * time.Second} + } + + return ctrl.Result{} +} + +func (rm resourceManager) registerPluginWithConsole(ctx context.Context, pluginInfo *UIPluginInfo) error { cluster := &operatorv1.Console{} if err := rm.k8sClient.Get(ctx, client.ObjectKey{Name: "cluster"}, cluster); err != nil { return err } - if !slices.Contains(cluster.Spec.Plugins, pluginInfo.ConsoleName) { - // Register the plugin with the console - cluster := &operatorv1.Console{ - TypeMeta: metav1.TypeMeta{ - APIVersion: operatorv1.GroupVersion.String(), - Kind: "Console", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "cluster", - }, - Spec: operatorv1.ConsoleSpec{ - OperatorSpec: operatorv1.OperatorSpec{ - ManagementState: operatorv1.Managed, - }, - Plugins: []string{ - pluginInfo.ConsoleName, - }, + if slices.Contains(cluster.Spec.Plugins, pluginInfo.ConsoleName) { + return nil + } + + clusterPlugins := append(cluster.Spec.Plugins, pluginInfo.ConsoleName) + + // Register the plugin with the console + cluster = &operatorv1.Console{ + TypeMeta: metav1.TypeMeta{ + APIVersion: operatorv1.GroupVersion.String(), + Kind: "Console", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: operatorv1.ConsoleSpec{ + OperatorSpec: operatorv1.OperatorSpec{ + ManagementState: operatorv1.Managed, }, - } + Plugins: clusterPlugins, + }, + } - if err := reconciler.NewMerger(cluster).Reconcile(ctx, rm.k8sClient, rm.scheme); err != nil { - return err - } + if err := reconciler.NewMerger(cluster).Reconcile(ctx, rm.k8sClient, rm.scheme); err != nil { + return err } return nil } -func (rm resourceManager) getUIPlugin(ctx context.Context, req ctrl.Request) (*obsui.ObservabilityUIPlugin, error) { +func (rm resourceManager) getUIPlugin(ctx context.Context, req ctrl.Request) (*uiv1alpha1.UIPlugin, error) { logger := rm.logger.WithValues("plugin", req.NamespacedName) - plugin := obsui.ObservabilityUIPlugin{} + plugin := uiv1alpha1.UIPlugin{} if err := rm.k8sClient.Get(ctx, req.NamespacedName, &plugin); err != nil { if apierrors.IsNotFound(err) { logger.V(3).Info("stack could not be found; may be marked for deletion") return nil, nil } - logger.Error(err, "failed to get ObservabilityUIPlugin") + logger.Error(err, "failed to get UIPlugin") return nil, err } diff --git a/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go b/pkg/controllers/uiplugin/plugin_info_builder.go similarity index 86% rename from pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go rename to pkg/controllers/uiplugin/plugin_info_builder.go index 480153f5c..925783c4a 100644 --- a/pkg/controllers/observability-ui/observability-ui-plugin/plugin_info_builder.go +++ b/pkg/controllers/uiplugin/plugin_info_builder.go @@ -1,16 +1,17 @@ -package observability_ui_plugin +package uiplugin import ( "fmt" osv1alpha1 "github.com/openshift/api/console/v1alpha1" - obsui "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) -type ObservabilityUIPluginInfo struct { +type UIPluginInfo struct { Image string Name string ConsoleName string @@ -20,15 +21,13 @@ type ObservabilityUIPluginInfo struct { ClusterRoleBinding *rbacv1.ClusterRoleBinding } -func PluginInfoBuilder(plugin *obsui.ObservabilityUIPlugin, pluginConf ObservabilityUIPluginsConfiguration, clusterVersion string) (*ObservabilityUIPluginInfo, error) { +func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfiguration, clusterVersion string) (*UIPluginInfo, error) { imageKey, err := getImageKeyForPluginType(plugin.Spec.Type, clusterVersion) - if err != nil { return nil, err } image := pluginConf.Images[imageKey] - if image == "" { return nil, fmt.Errorf("no image provided for plugin type %s with key %s", plugin.Spec.Type, imageKey) } @@ -36,11 +35,11 @@ func PluginInfoBuilder(plugin *obsui.ObservabilityUIPlugin, pluginConf Observabi name := "observability-ui-" + plugin.Name switch plugin.Spec.Type { - case obsui.TypeDashboards: + case uiv1alpha1.TypeDashboards: { readerRoleName := plugin.Name + "-datasource-reader" - pluginInfo := &ObservabilityUIPluginInfo{ + pluginInfo := &UIPluginInfo{ Image: image, Name: name, ConsoleName: "console-dashboards-plugin", diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index efda3a9e6..58d7b15ce 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -4,9 +4,7 @@ import ( "context" "fmt" - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -14,7 +12,7 @@ import ( stackctrl "github.com/rhobs/observability-operator/pkg/controllers/monitoring/monitoring-stack" tqctrl "github.com/rhobs/observability-operator/pkg/controllers/monitoring/thanos-querier" - obsuictrl "github.com/rhobs/observability-operator/pkg/controllers/observability-ui/observability-ui-plugin" + uictrl "github.com/rhobs/observability-operator/pkg/controllers/uiplugin" ) // NOTE: The instance selector label is hardcoded in static assets. @@ -37,14 +35,14 @@ type FeatureGates struct { } type OperatorConfiguration struct { - MetricsAddr string - HealthProbeAddr string - Prometheus stackctrl.PrometheusConfiguration - Alertmanager stackctrl.AlertmanagerConfiguration - ThanosSidecar stackctrl.ThanosConfiguration - ThanosQuerier tqctrl.ThanosConfiguration - ObservabilityUIPlugins obsuictrl.ObservabilityUIPluginsConfiguration - FeatureGates FeatureGates + MetricsAddr string + HealthProbeAddr string + Prometheus stackctrl.PrometheusConfiguration + Alertmanager stackctrl.AlertmanagerConfiguration + ThanosSidecar stackctrl.ThanosConfiguration + ThanosQuerier tqctrl.ThanosConfiguration + UIPlugins uictrl.UIPluginsConfiguration + FeatureGates FeatureGates } func WithPrometheusImage(image string) func(*OperatorConfiguration) { @@ -85,7 +83,7 @@ func WithHealthProbeAddr(addr string) func(*OperatorConfiguration) { func WithUIPluginImages(images map[string]string) func(*OperatorConfiguration) { return func(oc *OperatorConfiguration) { - oc.ObservabilityUIPlugins.Images = images + oc.UIPlugins.Images = images } } @@ -105,7 +103,7 @@ func NewOperatorConfiguration(opts ...func(*OperatorConfiguration)) *OperatorCon func New(cfg *OperatorConfiguration) (*Operator, error) { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: runtime.NewScheme(), + Scheme: NewScheme(cfg), Metrics: metricsserver.Options{ BindAddress: cfg.MetricsAddr, }, @@ -129,7 +127,7 @@ func New(cfg *OperatorConfiguration) (*Operator, error) { } if cfg.FeatureGates.OpenShift.Enabled { - if err := obsuictrl.RegisterWithManager(mgr, obsuictrl.Options{PluginsConf: cfg.ObservabilityUIPlugins}); err != nil { + if err := uictrl.RegisterWithManager(mgr, uictrl.Options{PluginsConf: cfg.UIPlugins}); err != nil { return nil, fmt.Errorf("unable to register observability-ui-plugin controller: %w", err) } } diff --git a/pkg/operator/scheme.go b/pkg/operator/scheme.go index 1e70b6c11..37310f9be 100644 --- a/pkg/operator/scheme.go +++ b/pkg/operator/scheme.go @@ -1,27 +1,26 @@ package operator import ( + osv1alpha1 "github.com/openshift/api/console/v1alpha1" + operatorv1 "github.com/openshift/api/operator/v1" + monitoringv1 "github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - osv1alpha1 "github.com/openshift/api/console/v1alpha1" - operatorv1 "github.com/openshift/api/operator/v1" - - monitoringv1 "github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring/v1" rhobsv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1" - rhobsuiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/observability-ui/v1alpha1" + uiv1alpha1 "github.com/rhobs/observability-operator/pkg/apis/uiplugin/v1alpha1" ) -func NewScheme(cfg OperatorConfiguration) *runtime.Scheme { +func NewScheme(cfg *OperatorConfiguration) *runtime.Scheme { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(rhobsv1alpha1.AddToScheme(scheme)) utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) - utilruntime.Must(rhobsuiv1alpha1.AddToScheme(scheme)) + utilruntime.Must(uiv1alpha1.AddToScheme(scheme)) if cfg.FeatureGates.OpenShift.Enabled { utilruntime.Must(osv1alpha1.AddToScheme(scheme)) diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index b1a5a8c44..4c74e3f30 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -84,7 +84,7 @@ func main(m *testing.M) int { func setupFramework() error { cfg := config.GetConfigOrDie() k8sClient, err := client.New(cfg, client.Options{ - Scheme: operator.NewScheme(operator.OperatorConfiguration{}), + Scheme: operator.NewScheme(&operator.OperatorConfiguration{}), }) if err != nil { return err From a7e618858f2938bb178736997f683e5774a6fd1a Mon Sep 17 00:00:00 2001 From: Gabriel Bernal Date: Thu, 18 Apr 2024 11:20:22 +0200 Subject: [PATCH 4/4] feat: make UIPlugin cluster scoped --- ...bility-operator.clusterserviceversion.yaml | 13 +++++- .../observability.openshift.io_uiplugins.yaml | 5 +- cmd/operator/main.go | 4 +- .../observability.openshift.io_uiplugins.yaml | 5 +- deploy/operator/kustomization.yaml | 4 -- .../observability-operator-cluster-role.yaml | 12 +++++ docs/api.md | 4 +- docs/user-guides/observability-ui-plugins.md | 1 - pkg/apis/uiplugin/v1alpha1/types.go | 2 +- pkg/controllers/uiplugin/components.go | 28 +++++------ pkg/controllers/uiplugin/controller.go | 8 ++-- .../uiplugin/plugin_info_builder.go | 46 +++++++++++-------- pkg/operator/operator.go | 3 +- pkg/reconciler/reconciler.go | 3 +- 14 files changed, 82 insertions(+), 56 deletions(-) diff --git a/bundle/manifests/observability-operator.clusterserviceversion.yaml b/bundle/manifests/observability-operator.clusterserviceversion.yaml index 8b17bb852..74692274e 100644 --- a/bundle/manifests/observability-operator.clusterserviceversion.yaml +++ b/bundle/manifests/observability-operator.clusterserviceversion.yaml @@ -491,6 +491,18 @@ spec: - patch - update - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + - roles + verbs: + - create + - delete + - list + - patch + - update + - watch - apiGroups: - security.openshift.io resourceNames: @@ -678,7 +690,6 @@ spec: - --images=alertmanager=quay.io/prometheus/alertmanager:v0.26.0 - --images=prometheus=quay.io/prometheus/prometheus:v2.49.1 - --images=thanos=quay.io/thanos/thanos:v0.33.0 - - --images=ui-dashboards=quay.io/openshift-observability-ui/console-dashboards-plugin:v0.1.0 env: - name: NAMESPACE valueFrom: diff --git a/bundle/manifests/observability.openshift.io_uiplugins.yaml b/bundle/manifests/observability.openshift.io_uiplugins.yaml index 1c30bb9c3..f95be9302 100644 --- a/bundle/manifests/observability.openshift.io_uiplugins.yaml +++ b/bundle/manifests/observability.openshift.io_uiplugins.yaml @@ -12,12 +12,12 @@ spec: listKind: UIPluginList plural: uiplugins singular: uiplugin - scope: Namespaced + scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: - description: UIPlugin defines an observability console plugin + description: UIPlugin defines an observability console plugin. properties: apiVersion: description: |- @@ -40,6 +40,7 @@ spec: description: UIPluginSpec is the specification for desired state of UIPlugin. properties: type: + description: Type defines the UI plugin. enum: - Dashboards type: string diff --git a/cmd/operator/main.go b/cmd/operator/main.go index ef2245e32..60d21c06b 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -86,7 +86,7 @@ func main() { flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&healthProbeAddr, "health-probe-bind-address", ":8081", "The address the health probe endpoint binds to.") flag.Var(images, "images", fmt.Sprintf("Full images refs to use for containers managed by the operator. E.g thanos=quay.io/thanos/thanos:v0.33.0. Images used are %v", imagesUsed())) - flag.BoolVar(&openShiftEnabled, "openshift.enabled", true, "Enable OpenShift specific features such as Console Plugins.") + flag.BoolVar(&openShiftEnabled, "openshift.enabled", false, "Enable OpenShift specific features such as Console Plugins.") opts := zap.Options{ Development: true, @@ -116,7 +116,7 @@ func main() { operator.WithAlertmanagerImage(imgMap["alertmanager"]), operator.WithThanosSidecarImage(imgMap["thanos"]), operator.WithThanosQuerierImage(imgMap["thanos"]), - operator.WithUIPluginImages(imgMap), + operator.WithUIPlugins(namespace, imgMap), operator.WithFeatureGates(operator.FeatureGates{ OpenShift: operator.OpenShiftFeatureGates{ Enabled: openShiftEnabled, diff --git a/deploy/crds/common/observability.openshift.io_uiplugins.yaml b/deploy/crds/common/observability.openshift.io_uiplugins.yaml index ccabd2cf2..c091aed3e 100644 --- a/deploy/crds/common/observability.openshift.io_uiplugins.yaml +++ b/deploy/crds/common/observability.openshift.io_uiplugins.yaml @@ -12,12 +12,12 @@ spec: listKind: UIPluginList plural: uiplugins singular: uiplugin - scope: Namespaced + scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: - description: UIPlugin defines an observability console plugin + description: UIPlugin defines an observability console plugin. properties: apiVersion: description: |- @@ -40,6 +40,7 @@ spec: description: UIPluginSpec is the specification for desired state of UIPlugin. properties: type: + description: Type defines the UI plugin. enum: - Dashboards type: string diff --git a/deploy/operator/kustomization.yaml b/deploy/operator/kustomization.yaml index 496b3b0b7..07758600a 100644 --- a/deploy/operator/kustomization.yaml +++ b/deploy/operator/kustomization.yaml @@ -38,10 +38,6 @@ patches: group: apps kind: Deployment version: v1 -- patch: |- - - op: add - path: /spec/template/spec/containers/0/args/- - value: --images=ui-dashboards=quay.io/openshift-observability-ui/console-dashboards-plugin:v0.1.0 target: group: apps kind: Deployment diff --git a/deploy/operator/observability-operator-cluster-role.yaml b/deploy/operator/observability-operator-cluster-role.yaml index 5c5fabc6e..f43662294 100644 --- a/deploy/operator/observability-operator-cluster-role.yaml +++ b/deploy/operator/observability-operator-cluster-role.yaml @@ -228,6 +228,18 @@ rules: - patch - update - watch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + - roles + verbs: + - create + - delete + - list + - patch + - update + - watch - apiGroups: - security.openshift.io resourceNames: diff --git a/docs/api.md b/docs/api.md index b01d8402f..ee74f8649 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2785,7 +2785,7 @@ Resource Types: -UIPlugin defines an observability console plugin +UIPlugin defines an observability console plugin. @@ -2852,7 +2852,7 @@ UIPluginSpec is the specification for desired state of UIPlugin. diff --git a/docs/user-guides/observability-ui-plugins.md b/docs/user-guides/observability-ui-plugins.md index be04e7440..7ca2dc154 100644 --- a/docs/user-guides/observability-ui-plugins.md +++ b/docs/user-guides/observability-ui-plugins.md @@ -17,7 +17,6 @@ apiVersion: observability.openshift.io/v1alpha1 kind: UIPlugin metadata: name: ui-dashboards - namespace: observability-ui spec: type: Dashboards ``` diff --git a/pkg/apis/uiplugin/v1alpha1/types.go b/pkg/apis/uiplugin/v1alpha1/types.go index fbc341ee9..2fdbe7cf0 100644 --- a/pkg/apis/uiplugin/v1alpha1/types.go +++ b/pkg/apis/uiplugin/v1alpha1/types.go @@ -12,7 +12,7 @@ import ( // UIPlugin defines an observability console plugin. // +k8s:openapi-gen=true -// +kubebuilder:resource +// +kubebuilder:resource:scope=Cluster // +kubebuilder:subresource:status type UIPlugin struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/controllers/uiplugin/components.go b/pkg/controllers/uiplugin/components.go index 7a6044962..5f0623640 100644 --- a/pkg/controllers/uiplugin/components.go +++ b/pkg/controllers/uiplugin/components.go @@ -22,14 +22,14 @@ const ( ) func pluginComponentReconcilers(plugin *uiv1alpha1.UIPlugin, pluginInfo UIPluginInfo) []reconciler.Reconciler { - hasClusterRole := pluginInfo.ClusterRole != nil - hasClusterRoleBinding := pluginInfo.ClusterRoleBinding != nil - namespace := plugin.Namespace + hasRole := pluginInfo.Role != nil + hasRoleBinding := pluginInfo.RoleBinding != nil + namespace := pluginInfo.ResourceNamespace return []reconciler.Reconciler{ reconciler.NewUpdater(newServiceAccount(pluginInfo, namespace), plugin), - reconciler.NewOptionalUpdater(newClusterRole(pluginInfo), plugin, hasClusterRole), - reconciler.NewOptionalUpdater(newClusterRoleBinding(pluginInfo), plugin, hasClusterRoleBinding), + reconciler.NewOptionalUpdater(newRole(pluginInfo), plugin, hasRole), + reconciler.NewOptionalUpdater(newRoleBinding(pluginInfo), plugin, hasRoleBinding), reconciler.NewUpdater(newDeployment(pluginInfo, namespace), plugin), reconciler.NewUpdater(newService(pluginInfo, namespace), plugin), reconciler.NewUpdater(newConsolePlugin(pluginInfo, namespace), plugin), @@ -49,12 +49,12 @@ func newServiceAccount(info UIPluginInfo, namespace string) *corev1.ServiceAccou } } -func newClusterRole(info UIPluginInfo) *rbacv1.ClusterRole { - return info.ClusterRole +func newRole(info UIPluginInfo) *rbacv1.Role { + return info.Role } -func newClusterRoleBinding(info UIPluginInfo) *rbacv1.ClusterRoleBinding { - return info.ClusterRoleBinding +func newRoleBinding(info UIPluginInfo) *rbacv1.RoleBinding { + return info.RoleBinding } func newConsolePlugin(info UIPluginInfo, namespace string) *osv1alpha1.ConsolePlugin { @@ -93,9 +93,7 @@ func newDeployment(info UIPluginInfo, namespace string) *appsv1.Deployment { Spec: appsv1.DeploymentSpec{ Replicas: ptr.To(int32(1)), Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app.kubernetes.io/instance": info.Name, - }, + MatchLabels: componentLabels(info.Name), }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -187,10 +185,8 @@ func newService(info UIPluginInfo, namespace string) *corev1.Service { TargetPort: intstr.FromInt32(port), }, }, - Selector: map[string]string{ - "app.kubernetes.io/instance": info.Name, - }, - Type: corev1.ServiceTypeClusterIP, + Selector: componentLabels(info.Name), + Type: corev1.ServiceTypeClusterIP, }, } } diff --git a/pkg/controllers/uiplugin/controller.go b/pkg/controllers/uiplugin/controller.go index 1798fbad8..91cb9a1f2 100644 --- a/pkg/controllers/uiplugin/controller.go +++ b/pkg/controllers/uiplugin/controller.go @@ -37,7 +37,8 @@ type resourceManager struct { } type UIPluginsConfiguration struct { - Images map[string]string + Images map[string]string + ResourcesNamespace string } type Options struct { @@ -58,6 +59,7 @@ const ( // RBAC for managing observability ui plugin objects //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=list;watch;create;update;delete;patch //+kubebuilder:rbac:groups="",resources=serviceaccounts;services;configmaps,verbs=get;list;watch;create;update;patch;delete // RBAC for managing Console CRs @@ -92,8 +94,8 @@ func RegisterWithManager(mgr ctrl.Manager, opts Options) error { Owns(&appsv1.Deployment{}, generationChanged). Owns(&v1.Service{}, generationChanged). Owns(&v1.ServiceAccount{}, generationChanged). - Owns(&rbacv1.ClusterRole{}, generationChanged). - Owns(&rbacv1.ClusterRoleBinding{}, generationChanged). + Owns(&rbacv1.Role{}, generationChanged). + Owns(&rbacv1.RoleBinding{}, generationChanged). Owns(&osv1alpha1.ConsolePlugin{}, generationChanged). Build(rm) if err != nil { diff --git a/pkg/controllers/uiplugin/plugin_info_builder.go b/pkg/controllers/uiplugin/plugin_info_builder.go index 925783c4a..470c76d9e 100644 --- a/pkg/controllers/uiplugin/plugin_info_builder.go +++ b/pkg/controllers/uiplugin/plugin_info_builder.go @@ -12,13 +12,14 @@ import ( ) type UIPluginInfo struct { - Image string - Name string - ConsoleName string - DisplayName string - Proxies []osv1alpha1.ConsolePluginProxy - ClusterRole *rbacv1.ClusterRole - ClusterRoleBinding *rbacv1.ClusterRoleBinding + Image string + Name string + ConsoleName string + DisplayName string + Proxies []osv1alpha1.ConsolePluginProxy + Role *rbacv1.Role + RoleBinding *rbacv1.RoleBinding + ResourceNamespace string } func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfiguration, clusterVersion string) (*UIPluginInfo, error) { @@ -33,17 +34,20 @@ func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfigur } name := "observability-ui-" + plugin.Name + namespace := pluginConf.ResourcesNamespace switch plugin.Spec.Type { case uiv1alpha1.TypeDashboards: { readerRoleName := plugin.Name + "-datasource-reader" + datasourcesNamespace := "openshift-config-managed" pluginInfo := &UIPluginInfo{ - Image: image, - Name: name, - ConsoleName: "console-dashboards-plugin", - DisplayName: "Console Enhanced Dashboards", + Image: image, + Name: name, + ConsoleName: "console-dashboards-plugin", + DisplayName: "Console Enhanced Dashboards", + ResourceNamespace: namespace, Proxies: []osv1alpha1.ConsolePluginProxy{ { Type: osv1alpha1.ProxyTypeService, @@ -51,18 +55,19 @@ func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfigur Authorize: true, Service: osv1alpha1.ConsolePluginProxyServiceConfig{ Name: name, - Namespace: plugin.Namespace, + Namespace: namespace, Port: 9443, }, }, }, - ClusterRole: &rbacv1.ClusterRole{ + Role: &rbacv1.Role{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacv1.SchemeGroupVersion.String(), - Kind: "ClusterRole", + Kind: "Role", }, ObjectMeta: metav1.ObjectMeta{ - Name: readerRoleName, + Name: readerRoleName, + Namespace: datasourcesNamespace, }, Rules: []rbacv1.PolicyRule{ { @@ -72,25 +77,26 @@ func PluginInfoBuilder(plugin *uiv1alpha1.UIPlugin, pluginConf UIPluginsConfigur }, }, }, - ClusterRoleBinding: &rbacv1.ClusterRoleBinding{ + RoleBinding: &rbacv1.RoleBinding{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacv1.SchemeGroupVersion.String(), - Kind: "ClusterRoleBinding", + Kind: "RoleBinding", }, ObjectMeta: metav1.ObjectMeta{ - Name: name + "-rolebinding", + Name: name + "-rolebinding", + Namespace: datasourcesNamespace, }, Subjects: []rbacv1.Subject{ { APIGroup: corev1.SchemeGroupVersion.Group, Kind: "ServiceAccount", Name: name + "-sa", - Namespace: plugin.Namespace, + Namespace: namespace, }, }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.SchemeGroupVersion.Group, - Kind: "ClusterRole", + Kind: "Role", Name: readerRoleName, }, }, diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 58d7b15ce..093ed025e 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -81,9 +81,10 @@ func WithHealthProbeAddr(addr string) func(*OperatorConfiguration) { } } -func WithUIPluginImages(images map[string]string) func(*OperatorConfiguration) { +func WithUIPlugins(namespace string, images map[string]string) func(*OperatorConfiguration) { return func(oc *OperatorConfiguration) { oc.UIPlugins.Images = images + oc.UIPlugins.ResourcesNamespace = namespace } } diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 9d4662347..bc80d322f 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -25,7 +25,8 @@ type Updater struct { } func (r Updater) Reconcile(ctx context.Context, c client.Client, scheme *runtime.Scheme) error { - if r.resourceOwner.GetNamespace() == r.resource.GetNamespace() { + // If the resource owner is in the same namespace as the resource, or if the resource owner is cluster scoped set the owner reference. + if r.resourceOwner.GetNamespace() == r.resource.GetNamespace() || r.resourceOwner.GetNamespace() == "" { if err := controllerutil.SetControllerReference(r.resourceOwner, r.resource, scheme); err != nil { return fmt.Errorf("%s/%s (%s): updater failed to set owner reference: %w", r.resource.GetNamespace(), r.resource.GetName(),
type enum -
+ Type defines the UI plugin.

Enum: Dashboards