diff --git a/.github/workflows/build-images-base.yaml b/.github/workflows/build-images-base.yaml index aaa8afbae..ed900e2c3 100644 --- a/.github/workflows/build-images-base.yaml +++ b/.github/workflows/build-images-base.yaml @@ -27,6 +27,10 @@ on: description: WASM Shim version default: latest type: string + consolePluginImageURL: + description: ConsolePlugin image URL + default: "quay.io/kuadrant/console-plugin:latest" + type: string channels: description: Bundle and catalog channels, comma separated default: preview @@ -65,6 +69,10 @@ on: description: WASM Shim version default: latest type: string + consolePluginImageURL: + description: ConsolePlugin image URL + default: "quay.io/kuadrant/console-plugin:latest" + type: string channels: description: Bundle and catalog channels, comma separated default: preview @@ -147,6 +155,7 @@ jobs: LIMITADOR_OPERATOR_VERSION=${{ inputs.limitadorOperatorVersion }} \ DNS_OPERATOR_VERSION=${{ inputs.dnsOperatorVersion }} \ WASM_SHIM_VERSION=${{ inputs.wasmShimVersion }} \ + RELATED_IMAGE_CONSOLEPLUGIN=${{ inputs.consolePluginImageURL }} \ DEFAULT_CHANNEL=${{ inputs.defaultChannel }} \ CHANNELS=${{ inputs.channels }} - name: Set up Docker Buildx @@ -195,6 +204,7 @@ jobs: LIMITADOR_OPERATOR_VERSION=${{ inputs.limitadorOperatorVersion }} \ DNS_OPERATOR_VERSION=${{ inputs.dnsOperatorVersion }} \ WASM_SHIM_VERSION=${{ inputs.wasmShimVersion }} \ + RELATED_IMAGE_CONSOLEPLUGIN=${{ inputs.consolePluginImageURL }} \ DEFAULT_CHANNEL=${{ inputs.defaultChannel }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 07f03fcf2..b21270c20 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,6 +26,10 @@ on: description: WASM Shim version default: latest type: string + consolePluginImageURL: + description: ConsolePlugin image URL + default: "quay.io/kuadrant/console-plugin:latest" + type: string prerelease: description: Is the release a pre-release? required: false @@ -60,6 +64,7 @@ jobs: LIMITADOR_OPERATOR_VERSION=${{ inputs.limitadorOperatorVersion }} \ DNS_OPERATOR_VERSION=${{ inputs.dnsOperatorVersion }} \ WASM_SHIM_VERSION=${{ inputs.wasmShimVersion }} \ + RELATED_IMAGE_CONSOLEPLUGIN=${{ inputs.consolePluginImageURL }} \ make prepare-release - name: Commit and push run: | @@ -72,7 +77,7 @@ jobs: with: name: v${{ inputs.kuadrantOperatorVersion }} tag_name: v${{ inputs.kuadrantOperatorVersion }} - body: "**This release enables installations of Authorino Operator v${{ inputs.authorinoOperatorVersion }}, Limitador Operator v${{ inputs.limitadorOperatorVersion }}, DNS Operator v${{ inputs.dnsOperatorVersion }} and WASM Shim v${{ inputs.wasmShimVersion }}**" + body: "**This release enables installations of Authorino Operator v${{ inputs.authorinoOperatorVersion }}, Limitador Operator v${{ inputs.limitadorOperatorVersion }}, DNS Operator v${{ inputs.dnsOperatorVersion }}, WASM Shim v${{ inputs.wasmShimVersion }} and ConsolePlugin ${{ inputs.consolePluginImageURL }}**" generate_release_notes: true target_commitish: release-v${{ github.event.inputs.kuadrantOperatorVersion }} prerelease: ${{ github.event.inputs.prerelease }} diff --git a/Makefile b/Makefile index f7422d169..0f5a81830 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,7 @@ endif # Kuadrant Namespace KUADRANT_NAMESPACE ?= kuadrant-system +OPERATOR_NAMESPACE ?= $(KUADRANT_NAMESPACE) # Kuadrant component versions ## authorino @@ -340,7 +341,7 @@ build: generate fmt vet ## Build manager binary. run: export LOG_LEVEL = debug run: export LOG_MODE = development -run: export OPERATOR_NAMESPACE = kuadrant-system +run: export OPERATOR_NAMESPACE := $(OPERATOR_NAMESPACE) run: GIT_SHA=$(shell git rev-parse HEAD || echo "unknown") run: DIRTY=$(shell $(PROJECT_PATH)/utils/check-git-dirty.sh || echo "unknown") run: generate fmt vet ## Run a controller from your host. @@ -383,12 +384,18 @@ rm -rf $$TMP_DIR ;\ } endef +RELATED_IMAGE_CONSOLEPLUGIN ?= quay.io/kuadrant/console-plugin:latest + .PHONY: bundle bundle: $(OPM) $(YQ) manifests dependencies-manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. $(OPERATOR_SDK) generate kustomize manifests -q - # Set desired operator image and related wasm shim image + # Set desired Wasm-shim image V="$(RELATED_IMAGE_WASMSHIM)" \ $(YQ) eval '(select(.kind == "Deployment").spec.template.spec.containers[].env[] | select(.name == "RELATED_IMAGE_WASMSHIM").value) = strenv(V)' -i config/manager/manager.yaml + # Set desired ConsolePlugin image + V="$(RELATED_IMAGE_CONSOLEPLUGIN)" \ + $(YQ) eval '(select(.kind == "Deployment").spec.template.spec.containers[].env[] | select(.name == "RELATED_IMAGE_CONSOLEPLUGIN").value) = strenv(V)' -i config/manager/manager.yaml + # Set desired operator image cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) # Update CSV $(call update-csv-config,kuadrant-operator.v$(BUNDLE_VERSION),config/manifests/bases/kuadrant-operator.clusterserviceversion.yaml,.metadata.name) @@ -443,11 +450,13 @@ prepare-release: ## Prepare the manifests for OLM and Helm Chart for a release. LIMITADOR_OPERATOR_VERSION=$(LIMITADOR_OPERATOR_VERSION) \ DNS_OPERATOR_VERSION=$(DNS_OPERATOR_VERSION) \ WASM_SHIM_VERSION=$(WASM_SHIM_VERSION) \ + RELATED_IMAGE_CONSOLEPLUGIN=$(RELATED_IMAGE_CONSOLEPLUGIN) \ $(MAKE) helm-build VERSION=$(VERSION) \ AUTHORINO_OPERATOR_VERSION=$(AUTHORINO_OPERATOR_VERSION) \ LIMITADOR_OPERATOR_VERSION=$(LIMITADOR_OPERATOR_VERSION) \ DNS_OPERATOR_VERSION=$(DNS_OPERATOR_VERSION) \ - WASM_SHIM_VERSION=$(WASM_SHIM_VERSION) + WASM_SHIM_VERSION=$(WASM_SHIM_VERSION) \ + RELATED_IMAGE_CONSOLEPLUGIN=$(RELATED_IMAGE_CONSOLEPLUGIN) sed -i -e 's/Version = ".*"/Version = "$(VERSION)"/' $(PROJECT_PATH)/version/version.go ##@ Code Style diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 46dd88642..60548529e 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -236,6 +236,18 @@ spec: - get - list - watch + - apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: @@ -648,6 +660,8 @@ spec: env: - name: RELATED_IMAGE_WASMSHIM value: oci://quay.io/kuadrant/wasm-shim:latest + - name: RELATED_IMAGE_CONSOLEPLUGIN + value: quay.io/kuadrant/console-plugin:latest - name: OPERATOR_NAMESPACE valueFrom: fieldRef: @@ -756,4 +770,6 @@ spec: relatedImages: - image: oci://quay.io/kuadrant/wasm-shim:latest name: wasmshim + - image: quay.io/kuadrant/console-plugin:latest + name: consoleplugin version: 0.0.0 diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 4de7b775e..6d455fcf1 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -15750,6 +15750,18 @@ rules: - get - list - watch +- apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: @@ -16212,6 +16224,8 @@ spec: env: - name: RELATED_IMAGE_WASMSHIM value: oci://quay.io/kuadrant/wasm-shim:latest + - name: RELATED_IMAGE_CONSOLEPLUGIN + value: quay.io/kuadrant/console-plugin:latest - name: OPERATOR_NAMESPACE valueFrom: fieldRef: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 8a89cf4f0..0177aaec2 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -24,6 +24,8 @@ spec: env: - name: RELATED_IMAGE_WASMSHIM value: "oci://quay.io/kuadrant/wasm-shim:latest" + - name: RELATED_IMAGE_CONSOLEPLUGIN + value: "quay.io/kuadrant/console-plugin:latest" - name: OPERATOR_NAMESPACE valueFrom: fieldRef: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 602db0f6d..2b2b0c1ef 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -83,6 +83,18 @@ rules: - get - list - watch +- apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/controllers/consoleplugin_reconciler.go b/controllers/consoleplugin_reconciler.go new file mode 100644 index 000000000..0c80e656e --- /dev/null +++ b/controllers/consoleplugin_reconciler.go @@ -0,0 +1,128 @@ +package controllers + +import ( + "context" + "sync" + + "github.com/go-logr/logr" + consolev1 "github.com/openshift/api/console/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/env" + "k8s.io/utils/ptr" + ctrlruntime "sigs.k8s.io/controller-runtime" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + + "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" + "github.com/kuadrant/kuadrant-operator/pkg/log" + "github.com/kuadrant/kuadrant-operator/pkg/openshift" + "github.com/kuadrant/kuadrant-operator/pkg/openshift/consoleplugin" +) + +//+kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;list;watch;create;update;patch;delete + +var ( + ConsolePluginImageURL = env.GetString("RELATED_IMAGE_CONSOLEPLUGIN", "quay.io/kuadrant/console-plugin:latest") +) + +type ConsolePluginReconciler struct { + *reconcilers.BaseReconciler + + namespace string +} + +func NewConsolePluginReconciler(mgr ctrlruntime.Manager, namespace string) *ConsolePluginReconciler { + return &ConsolePluginReconciler{ + BaseReconciler: reconcilers.NewBaseReconciler( + mgr.GetClient(), mgr.GetScheme(), mgr.GetAPIReader(), + log.Log.WithName("consoleplugin"), + mgr.GetEventRecorderFor("ConsolePlugin"), + ), + namespace: namespace, + } +} + +func (r *ConsolePluginReconciler) Subscription() *controller.Subscription { + return &controller.Subscription{ + ReconcileFunc: r.Run, + Events: []controller.ResourceEventMatcher{ + {Kind: ptr.To(openshift.ConsolePluginGVK.GroupKind())}, + { + Kind: ptr.To(ConfigMapGroupKind), + ObjectNamespace: r.namespace, + ObjectName: TopologyConfigMapName, + EventType: ptr.To(controller.CreateEvent), + }, + { + Kind: ptr.To(ConfigMapGroupKind), + ObjectNamespace: r.namespace, + ObjectName: TopologyConfigMapName, + EventType: ptr.To(controller.DeleteEvent), + }, + }, + } +} + +func (r *ConsolePluginReconciler) Run(eventCtx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, _ *sync.Map) error { + logger := r.Logger() + logger.V(1).Info("task started") + ctx := logr.NewContext(eventCtx, logger) + + existingTopologyConfigMaps := topology.Objects().Items(func(object machinery.Object) bool { + return object.GetName() == TopologyConfigMapName && object.GetNamespace() == r.namespace && object.GroupVersionKind().Kind == ConfigMapGroupKind.Kind + }) + + topologyExists := len(existingTopologyConfigMaps) > 0 + + // Service + service := consoleplugin.Service(r.namespace) + if !topologyExists { + utils.TagObjectToDelete(service) + } + err := r.ReconcileResource(ctx, &corev1.Service{}, service, reconcilers.CreateOnlyMutator) + if err != nil { + logger.Error(err, "reconciling service") + return err + } + + // Deployment + deployment := consoleplugin.Deployment(r.namespace, ConsolePluginImageURL) + deploymentMutators := make([]reconcilers.DeploymentMutateFn, 0) + deploymentMutators = append(deploymentMutators, reconcilers.DeploymentImageMutator) + if !topologyExists { + utils.TagObjectToDelete(deployment) + } + err = r.ReconcileResource(ctx, &appsv1.Deployment{}, deployment, reconcilers.DeploymentMutator(deploymentMutators...)) + if err != nil { + logger.Error(err, "reconciling deployment") + return err + } + + // Nginx ConfigMap + nginxConfigMap := consoleplugin.NginxConfigMap(r.namespace) + if !topologyExists { + utils.TagObjectToDelete(nginxConfigMap) + } + err = r.ReconcileResource(ctx, &corev1.ConfigMap{}, nginxConfigMap, reconcilers.CreateOnlyMutator) + if err != nil { + logger.Error(err, "reconciling nginx configmap") + return err + } + + // ConsolePlugin + consolePlugin := consoleplugin.ConsolePlugin(r.namespace) + if !topologyExists { + utils.TagObjectToDelete(consolePlugin) + } + err = r.ReconcileResource(ctx, &consolev1.ConsolePlugin{}, consolePlugin, reconcilers.CreateOnlyMutator) + if err != nil { + logger.Error(err, "reconciling consoleplugin") + return err + } + + logger.V(1).Info("task ended") + return nil +} diff --git a/controllers/consoleplugin_reconciler_test.go b/controllers/consoleplugin_reconciler_test.go new file mode 100644 index 000000000..13a84bc6e --- /dev/null +++ b/controllers/consoleplugin_reconciler_test.go @@ -0,0 +1,187 @@ +//go:build unit + +package controllers + +import ( + "context" + "testing" + + consolev1 "github.com/openshift/api/console/v1" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + controllersfake "github.com/kuadrant/kuadrant-operator/controllers/fake" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/openshift" + "github.com/kuadrant/kuadrant-operator/pkg/openshift/consoleplugin" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" +) + +var ( + TestNamespace = "test-namespace" +) + +type ConfigMap corev1.ConfigMap + +func (c *ConfigMap) GetLocator() string { + return machinery.LocatorFromObject(c) +} + +func buildTopologyWithTopologyConfigMap(t *testing.T) *machinery.Topology { + topologyConfigMap := &ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: ConfigMapGroupKind.Kind, + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: TopologyConfigMapName, + Namespace: TestNamespace, + Labels: map[string]string{kuadrant.TopologyLabel: "true"}, + }, + Data: map[string]string{}, + } + topology, err := machinery.NewTopology(machinery.WithObjects(topologyConfigMap)) + if err != nil { + t.Fatalf("failed to create topology: %v", err) + } + return topology +} + +// Since this reconciler only runs on Openshift, +// this unit test will add some coverage +func TestConsolePluginReconciler(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = appsv1.AddToScheme(scheme) + _ = gatewayapiv1.AddToScheme(scheme) + _ = consolev1.AddToScheme(scheme) + + manager := controllersfake. + NewManagerBuilder(). + WithClient(fake.NewClientBuilder().WithScheme(scheme).Build()). + WithScheme(scheme). + Build() + + reconciler := NewConsolePluginReconciler(manager, TestNamespace) + assert.Assert(t, reconciler != nil) + + t.Run("Subscription", func(subT *testing.T) { + subscription := reconciler.Subscription() + assert.Assert(subT, subscription != nil) + events := subscription.Events + assert.Assert(subT, is.Len(events, 3)) + assert.DeepEqual(subT, events[0].Kind, ptr.To(openshift.ConsolePluginGVK.GroupKind())) + assert.DeepEqual(subT, events[1].Kind, ptr.To(ConfigMapGroupKind)) + assert.DeepEqual(subT, events[1].ObjectName, TopologyConfigMapName) + assert.DeepEqual(subT, events[1].ObjectNamespace, TestNamespace) + assert.DeepEqual(subT, events[1].EventType, ptr.To(controller.CreateEvent)) + assert.DeepEqual(subT, events[2].Kind, ptr.To(ConfigMapGroupKind)) + assert.DeepEqual(subT, events[2].ObjectName, TopologyConfigMapName) + assert.DeepEqual(subT, events[2].ObjectNamespace, TestNamespace) + assert.DeepEqual(subT, events[2].EventType, ptr.To(controller.DeleteEvent)) + }) + + t.Run("Create service", func(subT *testing.T) { + topology := buildTopologyWithTopologyConfigMap(subT) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + service := &corev1.Service{} + serviceKey := client.ObjectKey{Name: consoleplugin.ServiceName(), Namespace: TestNamespace} + assert.NilError(subT, manager.GetClient().Get(context.TODO(), serviceKey, service)) + assert.DeepEqual(subT, service.GetLabels(), consoleplugin.CommonLabels()) + assert.DeepEqual(subT, service.GetAnnotations(), consoleplugin.ServiceAnnotations()) + assert.DeepEqual(subT, service.Spec.Selector, consoleplugin.ServiceSelector()) + assert.DeepEqual(subT, service.Spec.Ports, []corev1.ServicePort{ + { + Name: "9443-tcp", Protocol: corev1.ProtocolTCP, + Port: 9443, TargetPort: intstr.FromInt32(9443), + }, + }) + }) + + t.Run("Delete service", func(subT *testing.T) { + topology, err := machinery.NewTopology() + assert.Assert(subT, err == nil) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + service := &corev1.Service{} + serviceKey := client.ObjectKey{Name: consoleplugin.ServiceName(), Namespace: TestNamespace} + err = manager.GetClient().Get(context.TODO(), serviceKey, service) + assert.Assert(subT, apierrors.IsNotFound(err)) + }) + + t.Run("Create deployment", func(subT *testing.T) { + topology := buildTopologyWithTopologyConfigMap(subT) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + deployment := &appsv1.Deployment{} + deploymentKey := client.ObjectKey{Name: consoleplugin.DeploymentName(), Namespace: TestNamespace} + assert.NilError(subT, manager.GetClient().Get(context.TODO(), deploymentKey, deployment)) + assert.DeepEqual(subT, deployment.GetLabels(), consoleplugin.DeploymentLabels(TestNamespace)) + assert.DeepEqual(subT, deployment.Spec.Selector, consoleplugin.DeploymentSelector()) + assert.DeepEqual(subT, deployment.Spec.Strategy, consoleplugin.DeploymentStrategy()) + assert.Assert(subT, is.Len(deployment.Spec.Template.Spec.Containers, 1)) + assert.Assert(subT, deployment.Spec.Template.Spec.Containers[0].Image == ConsolePluginImageURL) + }) + + t.Run("Delete deployment", func(subT *testing.T) { + topology, err := machinery.NewTopology() + assert.Assert(subT, err == nil) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + deployment := &appsv1.Deployment{} + deploymentKey := client.ObjectKey{Name: consoleplugin.DeploymentName(), Namespace: TestNamespace} + err = manager.GetClient().Get(context.TODO(), deploymentKey, deployment) + assert.Assert(subT, apierrors.IsNotFound(err)) + }) + + t.Run("Create nginx configmap", func(subT *testing.T) { + topology := buildTopologyWithTopologyConfigMap(subT) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + configMap := &corev1.ConfigMap{} + cmKey := client.ObjectKey{Name: consoleplugin.NginxConfigMapName(), Namespace: TestNamespace} + assert.NilError(subT, manager.GetClient().Get(context.TODO(), cmKey, configMap)) + assert.DeepEqual(subT, configMap.GetLabels(), consoleplugin.CommonLabels()) + _, ok := configMap.Data["nginx.conf"] + assert.Assert(subT, ok) + }) + + t.Run("Delete nginx configmap", func(subT *testing.T) { + topology, err := machinery.NewTopology() + assert.Assert(subT, err == nil) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + configMap := &corev1.ConfigMap{} + cmKey := client.ObjectKey{Name: consoleplugin.NginxConfigMapName(), Namespace: TestNamespace} + err = manager.GetClient().Get(context.TODO(), cmKey, configMap) + assert.Assert(subT, apierrors.IsNotFound(err)) + }) + + t.Run("Create consoleplugin", func(subT *testing.T) { + topology := buildTopologyWithTopologyConfigMap(subT) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + consolePlugin := &consolev1.ConsolePlugin{} + consolePluginKey := client.ObjectKey{Name: consoleplugin.Name()} + assert.NilError(subT, manager.GetClient().Get(context.TODO(), consolePluginKey, consolePlugin)) + assert.DeepEqual(subT, consolePlugin.GetLabels(), consoleplugin.CommonLabels()) + assert.Assert(subT, consolePlugin.Spec.Backend.Service != nil) + assert.Assert(subT, consolePlugin.Spec.Backend.Service.Name == consoleplugin.ServiceName()) + assert.Assert(subT, consolePlugin.Spec.Backend.Service.Namespace == TestNamespace) + }) + + t.Run("Delete consoleplugin", func(subT *testing.T) { + topology, err := machinery.NewTopology() + assert.Assert(subT, err == nil) + assert.NilError(subT, reconciler.Run(context.TODO(), nil, topology, nil, nil)) + consolePlugin := &consolev1.ConsolePlugin{} + consolePluginKey := client.ObjectKey{Name: consoleplugin.Name()} + err = manager.GetClient().Get(context.TODO(), consolePluginKey, consolePlugin) + assert.Assert(subT, apierrors.IsNotFound(err)) + }) +} diff --git a/controllers/fake/api_reader.go b/controllers/fake/api_reader.go new file mode 100644 index 000000000..998d24f66 --- /dev/null +++ b/controllers/fake/api_reader.go @@ -0,0 +1,20 @@ +//go:build unit + +package fake + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type apireader struct { +} + +func (a *apireader) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + panic("Not Implemented") +} + +func (a *apireader) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + panic("Not Implemented") +} diff --git a/controllers/fake/event_recorder.go b/controllers/fake/event_recorder.go new file mode 100644 index 000000000..afd175695 --- /dev/null +++ b/controllers/fake/event_recorder.go @@ -0,0 +1,20 @@ +//go:build unit + +package fake + +import "k8s.io/apimachinery/pkg/runtime" + +type eventrecorder struct { +} + +func (e *eventrecorder) Event(object runtime.Object, eventtype, reason, message string) { + panic("Not Implemented") +} + +func (e *eventrecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { + panic("Not Implemented") +} + +func (e *eventrecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { + panic("Not Implemented") +} diff --git a/controllers/fake/manager.go b/controllers/fake/manager.go new file mode 100644 index 000000000..de85f78c2 --- /dev/null +++ b/controllers/fake/manager.go @@ -0,0 +1,99 @@ +//go:build unit + +package fake + +import ( + "context" + "net/http" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/healthz" + ctrlruntimemanager "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +type manager struct { + client client.Client + scheme *runtime.Scheme + apiReader client.Reader + eventRecorder record.EventRecorder +} + +func (m *manager) Add(ctrlruntimemanager.Runnable) error { + panic("Not Implemented") +} + +func (m *manager) Elected() <-chan struct{} { + panic("Not Implemented") +} + +func (m *manager) AddMetricsServerExtraHandler(path string, handler http.Handler) error { + panic("Not Implemented") + +} +func (m *manager) AddHealthzCheck(name string, check healthz.Checker) error { + panic("Not Implemented") +} + +func (m *manager) AddReadyzCheck(name string, check healthz.Checker) error { + panic("Not Implemented") +} + +func (m *manager) Start(ctx context.Context) error { + panic("Not Implemented") +} + +func (m *manager) GetWebhookServer() webhook.Server { + panic("Not Implemented") +} + +func (m *manager) GetLogger() logr.Logger { + panic("Not Implemented") +} + +func (m *manager) GetControllerOptions() config.Controller { + panic("Not Implemented") +} + +func (m *manager) GetHTTPClient() *http.Client { + panic("Not Implemented") +} + +func (m *manager) GetConfig() *rest.Config { + panic("Not Implemented") +} + +func (m *manager) GetCache() cache.Cache { + panic("Not Implemented") +} + +func (m *manager) GetScheme() *runtime.Scheme { + return m.scheme +} + +func (m *manager) GetClient() client.Client { + return m.client +} + +func (m *manager) GetFieldIndexer() client.FieldIndexer { + panic("Not Implemented") +} + +func (m *manager) GetEventRecorderFor(name string) record.EventRecorder { + return m.eventRecorder +} + +func (m *manager) GetRESTMapper() meta.RESTMapper { + panic("Not Implemented") +} + +func (m *manager) GetAPIReader() client.Reader { + return m.apiReader +} diff --git a/controllers/fake/manager_builder.go b/controllers/fake/manager_builder.go new file mode 100644 index 000000000..30ba67ff2 --- /dev/null +++ b/controllers/fake/manager_builder.go @@ -0,0 +1,40 @@ +//go:build unit + +package fake + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrlruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ManagerBuilder struct { + scheme *runtime.Scheme + client client.Client + apiReader client.Reader + eventRecorder record.EventRecorder +} + +func NewManagerBuilder() *ManagerBuilder { + return &ManagerBuilder{} +} + +func (m *ManagerBuilder) WithScheme(scheme *runtime.Scheme) *ManagerBuilder { + m.scheme = scheme + return m +} + +func (m *ManagerBuilder) WithClient(client client.Client) *ManagerBuilder { + m.client = client + return m +} + +func (m *ManagerBuilder) Build() ctrlruntime.Manager { + return &manager{ + client: m.client, + scheme: m.scheme, + apiReader: &apireader{}, + eventRecorder: &eventrecorder{}, + } +} diff --git a/controllers/state_of_the_world.go b/controllers/state_of_the_world.go index ddc6c7517..ae200864a 100644 --- a/controllers/state_of_the_world.go +++ b/controllers/state_of_the_world.go @@ -9,6 +9,7 @@ import ( authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" + consolev1 "github.com/openshift/api/console/v1" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" @@ -27,6 +28,8 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/istio" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/openshift" + "github.com/kuadrant/kuadrant-operator/pkg/openshift/consoleplugin" ) var ( @@ -115,8 +118,8 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D // TODO: add istio specific tasks to workflow } - ok, err = kuadrantgatewayapi.IsCertManagerInstalled(manager.GetRESTMapper(), logger) - if err != nil || !ok { + isCertManagerInstalled, err := kuadrantgatewayapi.IsCertManagerInstalled(manager.GetRESTMapper(), logger) + if err != nil || !isCertManagerInstalled { logger.Info("cert manager is not installed, skipping related watches and reconcilers", "err", err) } else { controllerOpts = append(controllerOpts, @@ -133,12 +136,24 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D // TODO: add tls policy specific tasks to workflow } - controllerOpts = append(controllerOpts, controller.WithReconcile(buildReconciler(client, isIstioInstalled, isEnvoyGatewayInstalled))) + isConsolePluginInstalled, err := openshift.IsConsolePluginInstalled(manager.GetRESTMapper()) + if err != nil || !isConsolePluginInstalled { + logger.Info("console plugin is not installed, skipping related watches and reconcilers", "err", err) + } else { + controllerOpts = append(controllerOpts, + controller.WithRunnable("consoleplugin watcher", controller.Watch( + &consolev1.ConsolePlugin{}, openshift.ConsolePluginsResource, metav1.NamespaceAll, + controller.FilterResourcesByLabel[*consolev1.ConsolePlugin](fmt.Sprintf("%s=%s", consoleplugin.AppLabelKey, consoleplugin.AppLabelValue)))), + controller.WithObjectKinds(openshift.ConsolePluginGVK.GroupKind()), + ) + } + + controllerOpts = append(controllerOpts, controller.WithReconcile(buildReconciler(manager, client, isIstioInstalled, isEnvoyGatewayInstalled, isConsolePluginInstalled))) return controller.NewController(controllerOpts...) } -func buildReconciler(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) controller.ReconcileFunc { +func buildReconciler(manager ctrlruntime.Manager, client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled, isConsolePluginInstalled bool) controller.ReconcileFunc { mainWorkflow := &controller.Workflow{ Precondition: initWorkflow(client).Run, Tasks: []controller.ReconcileFunc{ @@ -152,6 +167,12 @@ func buildReconciler(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGat Postcondition: finalStepsWorkflow(client, isIstioInstalled, isEnvoyGatewayInstalled).Run, } + if isConsolePluginInstalled { + mainWorkflow.Tasks = append(mainWorkflow.Tasks, + NewConsolePluginReconciler(manager, operatorNamespace).Subscription().Reconcile, + ) + } + return mainWorkflow.Run } diff --git a/controllers/test_common.go b/controllers/test_common.go index 41bab5e64..76f96d609 100644 --- a/controllers/test_common.go +++ b/controllers/test_common.go @@ -29,6 +29,7 @@ import ( egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + consolev1 "github.com/openshift/api/console/v1" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istiosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" @@ -306,6 +307,7 @@ func BootstrapScheme() *runtime.Scheme { certmanv1.AddToScheme, maistraapis.AddToScheme, egv1alpha1.AddToScheme, + consolev1.AddToScheme, ) err := sb.AddToScheme(s) diff --git a/controllers/topology_reconciler.go b/controllers/topology_reconciler.go index e61655717..f68931f8d 100644 --- a/controllers/topology_reconciler.go +++ b/controllers/topology_reconciler.go @@ -15,6 +15,10 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) +const ( + TopologyConfigMapName = "topology" +) + type TopologyReconciler struct { Client *dynamic.DynamicClient Namespace string @@ -32,7 +36,7 @@ func (r *TopologyReconciler) Reconcile(ctx context.Context, _ []controller.Resou cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: "topology", + Name: TopologyConfigMapName, Namespace: r.Namespace, Labels: map[string]string{kuadrant.TopologyLabel: "true"}, }, diff --git a/doc/development.md b/doc/development.md index 94e325e62..3d81f805c 100644 --- a/doc/development.md +++ b/doc/development.md @@ -119,6 +119,7 @@ The `make bundle` target accepts the following variables: | `AUTHORINO_OPERATOR_BUNDLE_IMG` | Authorino operator bundle URL | `quay.io/kuadrant/authorino-operator-bundle:latest` | `AUTHORINO_OPERATOR_VERSION` var could be used to build this, defaults to _latest_ if not provided | | `DNS_OPERATOR_BUNDLE_IMG` | DNS operator bundle URL | `quay.io/kuadrant/dns-operator-bundle:latest` | `DNS_OPERATOR_BUNDLE_IMG` var could be used to build this, defaults to _latest_ if not provided | | `RELATED_IMAGE_WASMSHIM` | WASM shim image URL | `oci://quay.io/kuadrant/wasm-shim:latest` | `WASM_SHIM_VERSION` var could be used to build this, defaults to _latest_ if not provided | +| `RELATED_IMAGE_CONSOLEPLUGIN` | ConsolePlugin image URL | `quay.io/kuadrant/console-plugin:latest` | | | `CHANNELS` | Bundle channels used in the bundle, comma separated | `alpha` | | | `DEFAULT_CHANNEL` | The default channel used in the bundle | `alpha` | | diff --git a/go.mod b/go.mod index a2ef1ae74..347ade174 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/martinlindhe/base36 v1.1.1 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.1 + github.com/openshift/api v0.0.0-20240926211938-f89ab92f1597 github.com/prometheus/client_golang v1.19.1 github.com/samber/lo v1.39.0 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index d33128aeb..91fd92781 100644 --- a/go.sum +++ b/go.sum @@ -349,6 +349,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.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/openshift/api v0.0.0-20240926211938-f89ab92f1597 h1:4T4Zeh5+fghPguNzQeDV3O/DRwUlwJ0sQDWFc7A8BBU= +github.com/openshift/api v0.0.0-20240926211938-f89ab92f1597/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= diff --git a/main.go b/main.go index 347f02985..60d3c981d 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( authorinoapi "github.com/kuadrant/authorino/api/v1beta2" kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" + consolev1 "github.com/openshift/api/console/v1" istioextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istionetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istiosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" @@ -91,6 +92,7 @@ func init() { utilruntime.Must(kuadrantdnsv1alpha1.AddToScheme(scheme)) utilruntime.Must(certmanv1.AddToScheme(scheme)) utilruntime.Must(egv1alpha1.AddToScheme(scheme)) + utilruntime.Must(consolev1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme logger := log.NewLogger( diff --git a/make/helm.mk b/make/helm.mk index aa7389beb..cfcb0aebe 100644 --- a/make/helm.mk +++ b/make/helm.mk @@ -2,9 +2,13 @@ .PHONY: helm-build helm-build: $(YQ) kustomize manifests ## Build the helm chart from kustomize manifests - # Set desired operator image and related wasm shim image + # Set desired Wasm-shim image V="$(RELATED_IMAGE_WASMSHIM)" \ $(YQ) eval '(select(.kind == "Deployment").spec.template.spec.containers[].env[] | select(.name == "RELATED_IMAGE_WASMSHIM").value) = strenv(V)' -i config/manager/manager.yaml + # Set desired ConsolePlugin image + V="$(RELATED_IMAGE_CONSOLEPLUGIN)" \ + $(YQ) eval '(select(.kind == "Deployment").spec.template.spec.containers[].env[] | select(.name == "RELATED_IMAGE_CONSOLEPLUGIN").value) = strenv(V)' -i config/manager/manager.yaml + # Set desired operator image cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) # Build the helm chart templates from kustomize manifests $(KUSTOMIZE) build config/helm > charts/kuadrant-operator/templates/manifests.yaml diff --git a/pkg/envoygateway/utils.go b/pkg/envoygateway/utils.go index 3fb068395..629bc3eff 100644 --- a/pkg/envoygateway/utils.go +++ b/pkg/envoygateway/utils.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) var ( @@ -19,7 +19,7 @@ var ( ) func IsEnvoyPatchPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, egv1alpha1.GroupName, egv1alpha1.KindEnvoyPatchPolicy, @@ -27,7 +27,7 @@ func IsEnvoyPatchPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { } func IsEnvoyExtensionPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, egv1alpha1.GroupName, egv1alpha1.KindEnvoyExtensionPolicy, @@ -35,7 +35,7 @@ func IsEnvoyExtensionPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { } func IsEnvoyGatewaySecurityPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, egv1alpha1.GroupName, egv1alpha1.KindSecurityPolicy, diff --git a/pkg/istio/utils.go b/pkg/istio/utils.go index 38fdd7378..e0cf22688 100644 --- a/pkg/istio/utils.go +++ b/pkg/istio/utils.go @@ -14,6 +14,7 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) var ( @@ -47,7 +48,7 @@ func PolicyTargetRefFromGateway(gateway *gatewayapiv1.Gateway) *istiocommon.Poli } func IsEnvoyFilterInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, istioclientnetworkingv1alpha3.GroupName, "EnvoyFilter", @@ -55,7 +56,7 @@ func IsEnvoyFilterInstalled(restMapper meta.RESTMapper) (bool, error) { } func IsWASMPluginInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, istioclientgoextensionv1alpha1.GroupName, "WasmPlugin", @@ -63,7 +64,7 @@ func IsWASMPluginInstalled(restMapper meta.RESTMapper) (bool, error) { } func IsAuthorizationPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { - return kuadrantgatewayapi.IsCRDInstalled( + return utils.IsCRDInstalled( restMapper, istioclientgosecurityv1beta1.GroupName, "AuthorizationPolicy", diff --git a/pkg/library/gatewayapi/utils.go b/pkg/library/gatewayapi/utils.go index cf016111c..f592103cf 100644 --- a/pkg/library/gatewayapi/utils.go +++ b/pkg/library/gatewayapi/utils.go @@ -11,7 +11,6 @@ import ( "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -154,21 +153,21 @@ func FilterValidSubdomains(domains, subdomains []gatewayapiv1.Hostname) []gatewa } func IsGatewayAPIInstalled(restMapper meta.RESTMapper) (bool, error) { - return IsCRDInstalled(restMapper, gatewayapiv1.GroupName, "HTTPRoute", gatewayapiv1.GroupVersion.Version) + return utils.IsCRDInstalled(restMapper, gatewayapiv1.GroupName, "HTTPRoute", gatewayapiv1.GroupVersion.Version) } func IsCertManagerInstalled(restMapper meta.RESTMapper, logger logr.Logger) (bool, error) { - if ok, err := IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.CertificateKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { + if ok, err := utils.IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.CertificateKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { logger.V(1).Error(err, "CertManager CRD was not installed", "group", certmanager.GroupName, "kind", certmanv1.CertificateKind, "version", certmanv1.SchemeGroupVersion.Version) return false, err } - if ok, err := IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.IssuerKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { + if ok, err := utils.IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.IssuerKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { logger.V(1).Error(err, "CertManager CRD was not installed", "group", certmanager.GroupName, "kind", certmanv1.IssuerKind, "version", certmanv1.SchemeGroupVersion.Version) return false, err } - if ok, err := IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.ClusterIssuerKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { + if ok, err := utils.IsCRDInstalled(restMapper, certmanager.GroupName, certmanv1.ClusterIssuerKind, certmanv1.SchemeGroupVersion.Version); !ok || err != nil { logger.V(1).Error(err, "CertManager CRD was not installed", "group", certmanager.GroupName, "kind", certmanv1.ClusterIssuerKind, "version", certmanv1.SchemeGroupVersion.Version) return false, err } @@ -176,22 +175,6 @@ func IsCertManagerInstalled(restMapper meta.RESTMapper, logger logr.Logger) (boo return true, nil } -func IsCRDInstalled(restMapper meta.RESTMapper, group, kind, version string) (bool, error) { - _, err := restMapper.RESTMapping( - schema.GroupKind{Group: group, Kind: kind}, - version, - ) - if err == nil { - return true, nil - } - - if meta.IsNoMatchError(err) { - return false, nil - } - - return false, err -} - // GetGatewayParentRefsFromRoute returns the list of parentRefs that are Gateway typed func GetGatewayParentRefsFromRoute(route *gatewayapiv1.HTTPRoute) []gatewayapiv1.ParentReference { if route == nil { diff --git a/pkg/library/reconcilers/deployment.go b/pkg/library/reconcilers/deployment.go new file mode 100644 index 000000000..9e4baf2dc --- /dev/null +++ b/pkg/library/reconcilers/deployment.go @@ -0,0 +1,45 @@ +package reconcilers + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func DeploymentImageMutator(desired, existing *appsv1.Deployment) bool { + update := false + + if existing.Spec.Template.Spec.Containers[0].Image != desired.Spec.Template.Spec.Containers[0].Image { + existing.Spec.Template.Spec.Containers[0].Image = desired.Spec.Template.Spec.Containers[0].Image + update = true + } + + return update +} + +// DeploymentMutateFn is a function which mutates the existing Deployment into it's desired state. +type DeploymentMutateFn func(desired, existing *appsv1.Deployment) bool + +func DeploymentMutator(opts ...DeploymentMutateFn) MutateFn { + return func(existingObj, desiredObj client.Object) (bool, error) { + existing, ok := existingObj.(*appsv1.Deployment) + if !ok { + return false, fmt.Errorf("%T is not a *appsv1.Deployment", existingObj) + } + desired, ok := desiredObj.(*appsv1.Deployment) + if !ok { + return false, fmt.Errorf("%T is not a *appsv1.Deployment", desiredObj) + } + + update := false + + // Loop through each option + for _, opt := range opts { + tmpUpdate := opt(desired, existing) + update = update || tmpUpdate + } + + return update, nil + } +} diff --git a/pkg/library/utils/crd.go b/pkg/library/utils/crd.go new file mode 100644 index 000000000..f1afb2116 --- /dev/null +++ b/pkg/library/utils/crd.go @@ -0,0 +1,22 @@ +package utils + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func IsCRDInstalled(restMapper meta.RESTMapper, group, kind, version string) (bool, error) { + _, err := restMapper.RESTMapping( + schema.GroupKind{Group: group, Kind: kind}, + version, + ) + if err == nil { + return true, nil + } + + if meta.IsNoMatchError(err) { + return false, nil + } + + return false, err +} diff --git a/pkg/openshift/consoleplugin/common.go b/pkg/openshift/consoleplugin/common.go new file mode 100644 index 000000000..c5e1b202d --- /dev/null +++ b/pkg/openshift/consoleplugin/common.go @@ -0,0 +1,22 @@ +package consoleplugin + +const ( + KuadrantConsoleName = "kuadrant-console" + KuadrantPluginComponent = "kuadrant-plugin" +) + +var ( + AppLabelKey = "app" + AppLabelValue = KuadrantConsoleName +) + +func CommonLabels() map[string]string { + return map[string]string{ + AppLabelKey: AppLabelValue, + "app.kubernetes.io/component": KuadrantPluginComponent, + "app.kubernetes.io/managed-by": "kuadrant-operator", + "app.kubernetes.io/instance": KuadrantConsoleName, + "app.kubernetes.io/name": KuadrantConsoleName, + "app.kubernetes.io/part-of": KuadrantConsoleName, + } +} diff --git a/pkg/openshift/consoleplugin/consoleplugin.go b/pkg/openshift/consoleplugin/consoleplugin.go new file mode 100644 index 000000000..5befac6e8 --- /dev/null +++ b/pkg/openshift/consoleplugin/consoleplugin.go @@ -0,0 +1,32 @@ +package consoleplugin + +import ( + consolev1 "github.com/openshift/api/console/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Name() string { + return KuadrantConsoleName +} + +func ConsolePlugin(ns string) *consolev1.ConsolePlugin { + return &consolev1.ConsolePlugin{ + TypeMeta: metav1.TypeMeta{Kind: "ConsolePlugin", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: Name(), + Labels: CommonLabels(), + }, + Spec: consolev1.ConsolePluginSpec{ + DisplayName: "Kuadrant Console Plugin", + Backend: consolev1.ConsolePluginBackend{ + Type: consolev1.Service, + Service: &consolev1.ConsolePluginService{ + Name: ServiceName(), + Namespace: ns, + Port: 9443, + BasePath: "/", + }, + }, + }, + } +} diff --git a/pkg/openshift/consoleplugin/deployment.go b/pkg/openshift/consoleplugin/deployment.go new file mode 100644 index 000000000..cd40bfb83 --- /dev/null +++ b/pkg/openshift/consoleplugin/deployment.go @@ -0,0 +1,120 @@ +package consoleplugin + +import ( + "maps" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" +) + +func DeploymentName() string { + return KuadrantConsoleName +} + +func DeploymentStrategy() appsv1.DeploymentStrategy { + return appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: ptr.To(intstr.FromString("25%")), + MaxSurge: ptr.To(intstr.FromString("25%")), + }, + } +} +func DeploymentSelector() *metav1.LabelSelector { + return &metav1.LabelSelector{ + MatchLabels: CommonLabels(), + } +} + +func DeploymentVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + { + Name: "plugin-serving-cert", + ReadOnly: true, + MountPath: "/var/serving-cert", + }, + { + Name: "nginx-conf", + ReadOnly: true, + MountPath: "/etc/nginx/nginx.conf", + SubPath: "nginx.conf", + }, + } +} + +func DeploymentVolumes() []corev1.Volume { + return []corev1.Volume{ + { + Name: "plugin-serving-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "plugin-serving-cert", + DefaultMode: ptr.To(int32(420)), + }, + }, + }, + { + Name: "nginx-conf", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: NginxConfigMapName(), + }, + DefaultMode: ptr.To(int32(420)), + }, + }, + }, + } +} + +func DeploymentLabels(namespace string) map[string]string { + result := map[string]string{ + "app.openshift.io/runtime-namespace": namespace, + } + + maps.Copy(result, CommonLabels()) + + return result +} + +func Deployment(ns, image string) *appsv1.Deployment { + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: DeploymentName(), + Namespace: ns, + Labels: DeploymentLabels(ns), + }, + Spec: appsv1.DeploymentSpec{ + Strategy: DeploymentStrategy(), + Selector: DeploymentSelector(), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: CommonLabels(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: KuadrantConsoleName, + Image: image, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 9443, + Protocol: corev1.ProtocolTCP, + }, + }, + ImagePullPolicy: corev1.PullAlways, + VolumeMounts: DeploymentVolumeMounts(), + }, + }, + Volumes: DeploymentVolumes(), + RestartPolicy: corev1.RestartPolicyAlways, + DNSPolicy: corev1.DNSClusterFirst, + }, + }, + }, + } +} diff --git a/pkg/openshift/consoleplugin/nginx_configmap.go b/pkg/openshift/consoleplugin/nginx_configmap.go new file mode 100644 index 000000000..f3e3c0fbf --- /dev/null +++ b/pkg/openshift/consoleplugin/nginx_configmap.go @@ -0,0 +1,42 @@ +package consoleplugin + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NginxConfigMapName() string { + return "kuadrant-console-nginx-conf" +} + +func NginxConfigMap(ns string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: NginxConfigMapName(), + Namespace: ns, + Labels: CommonLabels(), + }, + Data: map[string]string{ + "nginx.conf": `error_log /dev/stdout; +events {} +http { + access_log /dev/stdout; + include /etc/nginx/mime.types; + default_type application/octet-stream; + keepalive_timeout 65; + server { + listen 9443 ssl; + listen [::]:9443 ssl; + ssl_certificate /var/serving-cert/tls.crt; + ssl_certificate_key /var/serving-cert/tls.key; + add_header oauth_token "$http_Authorization"; + location / { + root /usr/share/nginx/html; + } + } +} +`, + }, + } +} diff --git a/pkg/openshift/consoleplugin/service.go b/pkg/openshift/consoleplugin/service.go new file mode 100644 index 000000000..37e898f76 --- /dev/null +++ b/pkg/openshift/consoleplugin/service.go @@ -0,0 +1,50 @@ +package consoleplugin + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func ServiceName() string { + return KuadrantConsoleName +} + +func ServiceAnnotations() map[string]string { + return map[string]string{ + "service.alpha.openshift.io/serving-cert-secret-name": "plugin-serving-cert", + } +} + +func ServiceSelector() map[string]string { + return map[string]string{ + "app": KuadrantConsoleName, + } +} + +func Service(ns string) *corev1.Service { + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName(), + Namespace: ns, + Labels: CommonLabels(), + Annotations: ServiceAnnotations(), + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "9443-tcp", + Protocol: corev1.ProtocolTCP, + Port: 9443, + TargetPort: intstr.FromInt32(9443), + }, + }, + Selector: ServiceSelector(), + Type: corev1.ServiceTypeClusterIP, + }, + } +} diff --git a/pkg/openshift/utils.go b/pkg/openshift/utils.go new file mode 100644 index 000000000..b9f1827de --- /dev/null +++ b/pkg/openshift/utils.go @@ -0,0 +1,22 @@ +package openshift + +import ( + consolev1 "github.com/openshift/api/console/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +var ( + ConsolePluginGVK schema.GroupVersionKind = schema.GroupVersionKind{ + Group: consolev1.GroupName, + Version: consolev1.GroupVersion.Version, + Kind: "ConsolePlugin", + } + ConsolePluginsResource = consolev1.SchemeGroupVersion.WithResource("consoleplugins") +) + +func IsConsolePluginInstalled(restMapper meta.RESTMapper) (bool, error) { + return utils.IsCRDInstalled(restMapper, ConsolePluginGVK.Group, ConsolePluginGVK.Kind, ConsolePluginGVK.Version) +}