diff --git a/cmd/openshift/operator/kodata/static/tekton-config/00-postreconcile/pipeline_console_plugin.yaml b/cmd/openshift/operator/kodata/static/tekton-config/00-postreconcile/pipeline_console_plugin.yaml new file mode 100644 index 0000000000..14a573118d --- /dev/null +++ b/cmd/openshift/operator/kodata/static/tekton-config/00-postreconcile/pipeline_console_plugin.yaml @@ -0,0 +1,139 @@ +# Copyright 2023 The Tekton Authors +# +# 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. + +# to know about dynamic plugin visit, +# https://github.com/openshift/enhancements/blob/master/enhancements/console/dynamic-plugins.md + +# service to access static contents: js, CSS, HTML. etc., +# this service creates and manages secret called "pipeline-console-plugin-cert" +# generated secret will be used in the console plugin container (nginx + static content) +--- +apiVersion: v1 +kind: Service +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + annotations: + # https://docs.openshift.com/container-platform/4.13/security/certificates/service-serving-certificate.html + service.beta.openshift.io/serving-cert-secret-name: pipeline-console-plugin-cert + labels: + app.kubernetes.io/part-of: tekton-config +spec: + ports: + - name: 8443-tcp + protocol: TCP + port: 8443 + targetPort: 8443 + selector: + name: pipeline-console-plugin + app: pipeline-console-plugin + +# nginx configuration +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + labels: + app.kubernetes.io/part-of: tekton-config +data: + nginx.conf: | + error_log /dev/stdout warn; + events {} + http { + access_log /dev/stdout; + include /etc/nginx/mime.types; + default_type application/octet-stream; + keepalive_timeout 65; + server { + listen 8443 ssl; + listen [::]:8443 ssl; + ssl_certificate /var/cert/tls.crt; + ssl_certificate_key /var/cert/tls.key; + root /usr/share/nginx/html; + } + } + +# nginx + pipeline dynamic console custom static contents +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + labels: + app.kubernetes.io/part-of: tekton-config +spec: + replicas: 1 + selector: + matchLabels: + name: pipeline-console-plugin + app: pipeline-console-plugin + template: + metadata: + labels: + name: pipeline-console-plugin + app: pipeline-console-plugin + app.kubernetes.io/part-of: tekton-config + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + volumes: + - name: pipeline-console-plugin-cert + secret: + secretName: pipeline-console-plugin-cert + defaultMode: 420 + - name: nginx-conf + configMap: + name: pipeline-console-plugin + defaultMode: 420 + containers: + - name: pipeline-console-plugin + image: ghcr.io/openshift-pipelines/console-plugin:main + imagePullPolicy: Always + ports: + - protocol: TCP + containerPort: 8443 + volumeMounts: + - name: pipeline-console-plugin-cert + readOnly: true + mountPath: /var/cert + - name: nginx-conf + readOnly: true + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + +# Console plugin is a cluster wide resource +# updates pipeline dynamic content provider service details +--- +apiVersion: console.openshift.io/v1 +kind: ConsolePlugin +metadata: + name: pipeline-console-plugin + labels: + app.kubernetes.io/part-of: tekton-config +spec: + displayName: Pipeline Console Plugin + backend: + type: Service + service: + name: pipeline-console-plugin + namespace: openshift-pipelines + port: 8443 + basePath: "/" + i18n: + loadType: Preload # options: Preload, Lazy diff --git a/config/openshift/base/operator.yaml b/config/openshift/base/operator.yaml index 30a260bf97..b15b3c0812 100644 --- a/config/openshift/base/operator.yaml +++ b/config/openshift/base/operator.yaml @@ -96,6 +96,8 @@ spec: value: registry.redhat.io/source-to-image/source-to-image-rhel8@sha256:6a6025914296a62fdf2092c3a40011bd9b966a6806b094d51eec5e1bd5026ef4 - name: IMAGE_ADDONS_PARAM_MAVEN_IMAGE value: registry.redhat.io/ubi8/openjdk-17@sha256:0d12c4097e098b62f78a7a31c0d711d78e1e5a53f4c007b9a5fc6cc6ab4dc018 + - name: IMAGE_PIPELINE_CONSOLE_PLUGIN + value: ghcr.io/openshift-pipelines/console-plugin:main - name: openshift-pipelines-operator-cluster-operations # tektoninstallerset reconciler image: ko://github.com/tektoncd/operator/cmd/openshift/operator args: diff --git a/config/openshift/base/role.yaml b/config/openshift/base/role.yaml index d23fe7a5e9..27d291d969 100644 --- a/config/openshift/base/role.yaml +++ b/config/openshift/base/role.yaml @@ -376,3 +376,16 @@ rules: - delete - update - patch +# to manage ConsolePlugin custom resource +- apiGroups: + - console.openshift.io + resources: + - consoleplugins + verbs: + - get + - list + - watch + - create + - delete + - update + - patch diff --git a/operatorhub/openshift/manifests/bases/openshift-pipelines-operator-rh.clusterserviceversion.template.yaml b/operatorhub/openshift/manifests/bases/openshift-pipelines-operator-rh.clusterserviceversion.template.yaml index b2e4932226..7cf1bf9069 100644 --- a/operatorhub/openshift/manifests/bases/openshift-pipelines-operator-rh.clusterserviceversion.template.yaml +++ b/operatorhub/openshift/manifests/bases/openshift-pipelines-operator-rh.clusterserviceversion.template.yaml @@ -6,6 +6,7 @@ metadata: capabilities: Full Lifecycle categories: Developer Tools, Integration & Delivery certified: "false" + console.openshift.io/plugins: '["pipeline-console-plugin"]' description: Red Hat OpenShift Pipelines is a cloud-native CI/CD solution for building pipelines using Tekton concepts which run natively on OpenShift and Kubernetes. operators.operatorframework.io/builder: operator-sdk-v1.7.2 operators.openshift.io/infrastructure-features: '["disconnected","proxy-aware"]' diff --git a/pkg/reconciler/common/testdata/test-replace-namespace.yaml b/pkg/reconciler/common/testdata/test-replace-namespace.yaml new file mode 100644 index 0000000000..829a7777f6 --- /dev/null +++ b/pkg/reconciler/common/testdata/test-replace-namespace.yaml @@ -0,0 +1,58 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: sample-sa + namespace: tekton-pipelines + +--- +apiVersion: v1 +kind: Service +metadata: + name: sample-service + namespace: tekton-pipelines + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: sample-config-map + namespace: tekton-pipelines + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sample-config-map + namespace: tekton-pipelines + +--- +apiVersion: v1 +kind: Pod +metadata: + name: sample-pod + namespace: tekton-pipelines + +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-secret + namespace: tekton-pipelines + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: sample-cluster-role-binding +subjects: + - kind: ServiceAccount + name: tekton-resource-pruner + namespace: hello + - kind: ServiceAccount + name: tekton-resource-pruner2 + namespace: tekton-pipelines +roleRef: + kind: ClusterRole + name: tekton-resource-pruner + apiGroup: rbac.authorization.k8s.io diff --git a/pkg/reconciler/common/transformers.go b/pkg/reconciler/common/transformers.go index ef1cd5780a..a311308195 100644 --- a/pkg/reconciler/common/transformers.go +++ b/pkg/reconciler/common/transformers.go @@ -874,3 +874,38 @@ func ReplaceNamespaceInClusterRoleBinding(targetNamespace string) mf.Transformer return nil } } + +// updates "metadata.namespace" and under "spec" +// TODO: we have different transformer for each kind +// TODO: replaces all the existing transformers(used to update namespace) with this. +func ReplaceNamespace(newNamespace string) mf.Transformer { + return func(u *unstructured.Unstructured) error { + // update metadata.namespace for all the resources + // this change will be updated in cluster wide resource too + // there is no effect on updating namespace on cluster wide resource + u.SetNamespace(newNamespace) + + switch u.GetKind() { + case "ClusterRoleBinding": + crb := &rbacv1.ClusterRoleBinding{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, crb) + if err != nil { + return err + } + + // update namespace + for index := range crb.Subjects { + crb.Subjects[index].Namespace = newNamespace + } + + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(crb) + if err != nil { + return err + } + u.SetUnstructuredContent(obj) + + } + + return nil + } +} diff --git a/pkg/reconciler/common/transformers_test.go b/pkg/reconciler/common/transformers_test.go index f0339bbe23..f3fb5f5c64 100644 --- a/pkg/reconciler/common/transformers_test.go +++ b/pkg/reconciler/common/transformers_test.go @@ -811,3 +811,50 @@ func TestCopyConfigMap(t *testing.T) { }) } } + +func TestReplaceNamespace(t *testing.T) { + tests := []struct { + name string + targetNamespace string + }{ + { + name: "target-ns-foo", + targetNamespace: "foo", + }, + { + name: "target-ns-bar", + targetNamespace: "bar", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // get a manifest + testData := path.Join("testdata", "test-replace-namespace.yaml") + manifest, err := mf.ManifestFrom(mf.Recursive(testData)) + assert.NilError(t, err) + + manifest, err = manifest.Transform(ReplaceNamespace(test.targetNamespace)) + assert.NilError(t, err) + + // verify the changes + for _, resource := range manifest.Resources() { + // assert namespace + assert.Equal(t, test.targetNamespace, resource.GetNamespace()) + + switch resource.GetKind() { + case "ClusterRoleBinding": + crb := &rbacv1.ClusterRoleBinding{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(resource.Object, crb) + assert.NilError(t, err) + // verify namespace + for index := range crb.Subjects { + assert.Equal(t, test.targetNamespace, crb.Subjects[index].Namespace) + + } + } + + } + }) + } +} diff --git a/pkg/reconciler/openshift/tektonconfig/extension.go b/pkg/reconciler/openshift/tektonconfig/extension.go index 91e1cc705d..2b33da7c9c 100644 --- a/pkg/reconciler/openshift/tektonconfig/extension.go +++ b/pkg/reconciler/openshift/tektonconfig/extension.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "path/filepath" mf "github.com/manifestival/manifestival" security "github.com/openshift/client-go/security/clientset/versioned" @@ -49,7 +50,8 @@ func OpenShiftExtension(ctx context.Context) common.Extension { if err != nil { logger.Fatal(err) } - return openshiftExtension{ + + ext := openshiftExtension{ operatorClientSet: operatorclient.Get(ctx), kubeClientSet: kubeclient.Get(ctx), rbacInformer: rbacInformer.Get(ctx), @@ -57,13 +59,23 @@ func OpenShiftExtension(ctx context.Context) common.Extension { securityClientSet: pkgCommon.GetSecurityClient(ctx), operatorVersion: operatorVer, } + + ext.manifestsPostReconciler = &manifestsPostReconciler{ + resourcesYamlDirectory: filepath.Join(common.ComponentBaseDir(), manifestsPostReconcileYamlDirectory), + logger: logger, + operatorClientSet: ext.operatorClientSet, + operatorVersion: operatorVer, + } + + return ext } type openshiftExtension struct { - operatorClientSet versioned.Interface - kubeClientSet kubernetes.Interface - rbacInformer rbacV1.ClusterRoleBindingInformer - nsInformer nsV1.NamespaceInformer + operatorClientSet versioned.Interface + kubeClientSet kubernetes.Interface + rbacInformer rbacV1.ClusterRoleBindingInformer + nsInformer nsV1.NamespaceInformer + manifestsPostReconciler *manifestsPostReconciler // OpenShift clientsets are a bit... special, we need to get each // clientset separately @@ -163,7 +175,9 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te return err } } - return nil + + // execute manifests post reconciler + return oe.manifestsPostReconciler.reconcile(ctx, configInstance) } func (oe openshiftExtension) Finalize(ctx context.Context, comp v1alpha1.TektonComponent) error { diff --git a/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler.go b/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler.go new file mode 100644 index 0000000000..135ea1d760 --- /dev/null +++ b/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler.go @@ -0,0 +1,219 @@ +package tektonconfig + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "github.com/go-logr/zapr" + mf "github.com/manifestival/manifestival" + + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + "github.com/tektoncd/operator/pkg/client/clientset/versioned" + "github.com/tektoncd/operator/pkg/reconciler/common" + "github.com/tektoncd/operator/pkg/reconciler/shared/hash" + "go.uber.org/zap" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + // manifests post reconcile yaml directory location + manifestsPostReconcileYamlDirectory = "static/tekton-config/00-postreconcile" + // installerSet label value + manifestsPostReconcileLabelCreatedByValue = "tekton-config-post-reconcile-manifests" + // pipeline console plugin environment variable key + PipelineConsolePluginImageEnvironmentKey = "IMAGE_PIPELINE_CONSOLE_PLUGIN" + // pipeline console plugin container name, used to replace the image from the environment + PipelineConsolePluginContainerName = "pipeline-console-plugin" +) + +var ( + // label filter to set/get installerSet specific to this reconciler + manifestsPostReconcileInstallerSetLabel = metav1.LabelSelector{ + MatchLabels: map[string]string{ + v1alpha1.InstallerSetType: v1alpha1.ConfigResourceName, + v1alpha1.CreatedByKey: manifestsPostReconcileLabelCreatedByValue, + }, + } +) + +type manifestsPostReconciler struct { + logger *zap.SugaredLogger + operatorClientSet versioned.Interface + syncOnce sync.Once + resourcesYamlDirectory string + operatorVersion string + pipelineConsolePluginImage string + manifest mf.Manifest +} + +// reconcile steps +// 1. get post reconcile (pipeline console plugin) manifests from kodata +// 2. verify the existing installerSet hash value +// 3. if there is a mismatch or the installerSet not available, (re)create it +func (prm *manifestsPostReconciler) reconcile(ctx context.Context, tektonConfigCR *v1alpha1.TektonConfig) error { + + prm.updateOnce(ctx) + + // verify he availability of the installerSet + labelSelector, err := common.LabelSelector(manifestsPostReconcileInstallerSetLabel) + if err != nil { + return err + } + + installerSetList, err := prm.operatorClientSet.OperatorV1alpha1().TektonInstallerSets().List(ctx, metav1.ListOptions{LabelSelector: labelSelector}) + if err != nil { + return err + } + + doCreateInstallerSet := false + var deployedInstallerSet v1alpha1.TektonInstallerSet + + if len(installerSetList.Items) > 1 { + for _, installerSet := range installerSetList.Items { + err = prm.operatorClientSet.OperatorV1alpha1().TektonInstallerSets().Delete(ctx, installerSet.GetName(), metav1.DeleteOptions{}) + if err != nil { + return err + } + } + doCreateInstallerSet = true + } else if len(installerSetList.Items) == 1 { + deployedInstallerSet = installerSetList.Items[0] + } else { + doCreateInstallerSet = true + } + + // clone the manifest + manifest := prm.manifest.Append() + // apply transformations + if err := prm.transform(ctx, &manifest, tektonConfigCR); err != nil { + tektonConfigCR.Status.MarkNotReady(fmt.Sprintf("transformation failed: %s", err.Error())) + return err + } + + // get expected hash value of the manifests + expectedHash, err := prm.getHash(manifest.Resources()) + if err != nil { + return err + } + + if !doCreateInstallerSet { + // compute hash from the deployed installerSet + deployedHash, err := prm.getHash(deployedInstallerSet.Spec.Manifests) + if err != nil { + return err + } + + releaseVersion := deployedInstallerSet.GetLabels()[v1alpha1.ReleaseVersionKey] + // delete the existing installerSet, + // if hash mismatch or version mismatch + if expectedHash != deployedHash || prm.operatorVersion != releaseVersion { + if err := prm.operatorClientSet.OperatorV1alpha1().TektonInstallerSets().Delete(ctx, deployedInstallerSet.GetName(), metav1.DeleteOptions{}); err != nil { + return err + } + doCreateInstallerSet = true + } + } + + if doCreateInstallerSet { + return prm.createInstallerSet(ctx, &manifest, tektonConfigCR) + } + + return nil +} + +func (prm *manifestsPostReconciler) updateOnce(ctx context.Context) { + // reads all yaml files from the directory, it is an expensive process to access disk on each reconcile call. + // hence fetch only once at startup, it helps not to degrade the performance of the reconcile loop + // also it not necessary to read the files frequently, as the files are shipped along the container and never change + prm.syncOnce.Do(func() { + // fetch manifest from disk + manifest, err := mf.NewManifest(prm.resourcesYamlDirectory, mf.UseLogger(zapr.NewLogger(prm.logger.Desugar()))) + if err != nil { + prm.logger.Fatal("error getting manifests", + "manifestsLocation", prm.resourcesYamlDirectory, + err, + ) + } + prm.manifest = manifest + + // update pipeline console image details + consoleImage, found := os.LookupEnv(PipelineConsolePluginImageEnvironmentKey) + if found { + prm.pipelineConsolePluginImage = consoleImage + prm.logger.Debugw("pipeline console plugin image found from environment", + "image", consoleImage, + "environmentVariable", PipelineConsolePluginImageEnvironmentKey, + ) + } else { + prm.logger.Warnw("pipeline console plugin image not found from environment, continuing with the default image from the manifest", + "environmentVariable", PipelineConsolePluginImageEnvironmentKey, + ) + } + }) +} + +func (prm *manifestsPostReconciler) createInstallerSet(ctx context.Context, manifest *mf.Manifest, tektonConfigCR *v1alpha1.TektonConfig) error { + // setup installerSet + ownerRef := *metav1.NewControllerRef(tektonConfigCR, tektonConfigCR.GetGroupVersionKind()) + installerSet := &v1alpha1.TektonInstallerSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "tekton-config-post-reconcile-manifests-", + Labels: manifestsPostReconcileInstallerSetLabel.MatchLabels, + Annotations: map[string]string{ + v1alpha1.TargetNamespaceKey: tektonConfigCR.Spec.TargetNamespace, + }, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + Spec: v1alpha1.TektonInstallerSetSpec{ + Manifests: manifest.Resources(), + }, + } + // update operator version + installerSet.Labels[v1alpha1.ReleaseVersionKey] = prm.operatorVersion + + // creates installerSet in the cluster + _, err := prm.operatorClientSet.OperatorV1alpha1().TektonInstallerSets().Create(ctx, installerSet, metav1.CreateOptions{}) + if err != nil { + prm.logger.Error("error on creating installerset", err) + } + return err +} + +// apply transformations +func (prm *manifestsPostReconciler) transform(ctx context.Context, manifest *mf.Manifest, tektonConfigCR *v1alpha1.TektonConfig) error { + // load required transformers + transformers := []mf.Transformer{ + // updates "metadata.namespace" to targetNamespace + common.ReplaceNamespace(tektonConfigCR.Spec.TargetNamespace), + prm.transformerConsolePlugin(tektonConfigCR.Spec.TargetNamespace), + } + + if prm.pipelineConsolePluginImage != "" { + // updates deployments container image + transformers = append(transformers, common.DeploymentImages(map[string]string{ + // on the transformer, in the container name, the '-' replaced with '_' + strings.ReplaceAll(PipelineConsolePluginContainerName, "-", "_"): prm.pipelineConsolePluginImage, + })) + } + + // perform transformation + return common.Transform(ctx, manifest, tektonConfigCR, transformers...) +} + +func (prm *manifestsPostReconciler) getHash(resources []unstructured.Unstructured) (string, error) { + return hash.Compute(resources) +} + +func (prm *manifestsPostReconciler) transformerConsolePlugin(targetNamespace string) mf.Transformer { + return func(u *unstructured.Unstructured) error { + if u.GetKind() != "ConsolePlugin" { + return nil + } + + return unstructured.SetNestedField(u.Object, targetNamespace, "spec", "backend", "service", "namespace") + } +} diff --git a/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler_test.go b/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler_test.go new file mode 100644 index 0000000000..3e1a49e9ae --- /dev/null +++ b/pkg/reconciler/openshift/tektonconfig/manifests_post_reconciler_test.go @@ -0,0 +1,188 @@ +package tektonconfig + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + "github.com/tektoncd/operator/pkg/client/clientset/versioned/fake" + "github.com/tektoncd/operator/pkg/reconciler/common" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apimachineryRuntime "k8s.io/apimachinery/pkg/runtime" + k8sTesting "k8s.io/client-go/testing" + "knative.dev/pkg/logging" +) + +// reactor required for the GenerateName field to work when using the fake client +func generateNameReactor(action k8sTesting.Action) (bool, apimachineryRuntime.Object, error) { + resource := action.(k8sTesting.CreateAction).GetObject() + meta, ok := resource.(metav1.Object) + if !ok { + return false, resource, nil + } + + if meta.GetName() == "" && meta.GetGenerateName() != "" { + meta.SetName(common.SimpleNameGenerator.RestrictLengthWithRandomSuffix(meta.GetGenerateName())) + } + return false, resource, nil +} + +func TestPostReconcileManifest(t *testing.T) { + defaultConsolePluginImage := "ghcr.io/openshift-pipelines/console-plugin:main" + + tests := []struct { + name string + consolePluginImage string + operatorVersion string + targetNamespace string + }{ + { + name: "test-without-console-plugin-image", + operatorVersion: "1.14.0", + targetNamespace: "foo", + }, + { + name: "test-with-console-plugin-image", + consolePluginImage: "custom-image:tag1", + operatorVersion: "0.70.0", + targetNamespace: "bar", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + ctx := context.TODO() + operatorFakeClientSet := fake.NewSimpleClientset() + + // add reactor to update generateName + operatorFakeClientSet.PrependReactor("create", "*", generateNameReactor) + + // TEST: verifies required values in generated manifests (InstallerSet) + verifyManifestFunc := func(expectedImage, expectedOperatorVersion string) { + // verify installersets availability + installerSetList, err := operatorFakeClientSet.OperatorV1alpha1().TektonInstallerSets().List( + ctx, + metav1.ListOptions{LabelSelector: fmt.Sprintf("operator.tekton.dev/created-by=%s", manifestsPostReconcileLabelCreatedByValue)}, + ) + require.NoError(t, err) + + require.Equal(t, 1, len(installerSetList.Items)) + installerSet := installerSetList.Items[0] + + // verify operator version label + operatorVersion := installerSet.GetLabels()[v1alpha1.ReleaseVersionKey] + require.Equal(t, expectedOperatorVersion, operatorVersion) + + // get installerset and verify transform values + for _, u := range installerSet.Spec.Manifests { + // verify targetNamespace + require.Equal(t, test.targetNamespace, u.GetNamespace()) + + switch u.GetKind() { + case "Deployment": + deployment := &appsv1.Deployment{} + err := apimachineryRuntime.DefaultUnstructuredConverter.FromUnstructured(u.Object, deployment) + require.NoError(t, err) + require.Equal(t, "pipeline-console-plugin", deployment.GetName()) + container := deployment.Spec.Template.Spec.Containers[0] + require.Equal(t, expectedImage, container.Image) + + case "ConsolePlugin": + actualNamespace, found, err := unstructured.NestedString(u.Object, "spec", "backend", "service", "namespace") + require.NoError(t, err) + require.True(t, found) + require.Equal(t, test.targetNamespace, actualNamespace) + + } + } + } + + // reconciler reference + postReconcile := &manifestsPostReconciler{ + logger: logging.FromContext(ctx).Named("post-reconcile-manifest-test"), + operatorClientSet: operatorFakeClientSet, + syncOnce: sync.Once{}, + resourcesYamlDirectory: "./testdata/postreconcile_manifest", + operatorVersion: test.operatorVersion, + } + + // tekton config CR + tektonConfigCR := &v1alpha1.TektonConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1alpha1.ConfigResourceName, + }, + Spec: v1alpha1.TektonConfigSpec{ + CommonSpec: v1alpha1.CommonSpec{ + TargetNamespace: test.targetNamespace, + }, + }, + } + + // console plugin image + consolePluginImage := defaultConsolePluginImage + // update image env variable + if test.consolePluginImage != "" { + t.Setenv("IMAGE_PIPELINE_CONSOLE_PLUGIN", test.consolePluginImage) + consolePluginImage = test.consolePluginImage + } + // TEST: image name + err := postReconcile.reconcile(ctx, tektonConfigCR) // perform reconcile + require.NoError(t, err) + verifyManifestFunc(consolePluginImage, test.operatorVersion) // verify manifests + + // TEST: operator version change + // update operator version in installerSet and reconcile + postReconcile.operatorVersion = "foo" + err = postReconcile.reconcile(ctx, tektonConfigCR) // perform reconcile + require.NoError(t, err) + verifyManifestFunc(consolePluginImage, "foo") // verify + postReconcile.operatorVersion = test.operatorVersion + + // TEST: removal of extra installerSet + // add another tekton config manifest post reconcile installerset and reconcile + newInstallerSet := &v1alpha1.TektonInstallerSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "another-tekton-config-manifest-foo-", + Labels: manifestsPostReconcileInstallerSetLabel.MatchLabels, + }, + } + _, err = operatorFakeClientSet.OperatorV1alpha1().TektonInstallerSets().Create(ctx, newInstallerSet, metav1.CreateOptions{}) + require.NoError(t, err) + err = postReconcile.reconcile(ctx, tektonConfigCR) // perform reconcile + require.NoError(t, err) + verifyManifestFunc(consolePluginImage, test.operatorVersion) // verify manifests + + // TEST: do not touch others installerSets + // add another installerset(not tekton config manifest post reconcile) and reconcile + // this installerSet should not be removed + anotherInstallerSetName := "pipelines-foo" + anotherInstallerSet := &v1alpha1.TektonInstallerSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: anotherInstallerSetName, + }, + } + _, err = operatorFakeClientSet.OperatorV1alpha1().TektonInstallerSets().Create(ctx, anotherInstallerSet, metav1.CreateOptions{}) + require.NoError(t, err) + err = postReconcile.reconcile(ctx, tektonConfigCR) // perform reconcile + require.NoError(t, err) + verifyManifestFunc(consolePluginImage, test.operatorVersion) // verify manifests + installerSetList, err := operatorFakeClientSet.OperatorV1alpha1().TektonInstallerSets().List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(installerSetList.Items)) + expectedInstallerSetFound := false + for _, installerSet := range installerSetList.Items { + if installerSet.GetName() == anotherInstallerSetName { + expectedInstallerSetFound = true + break + } + } + require.True(t, expectedInstallerSetFound) + }) + } +} diff --git a/pkg/reconciler/openshift/tektonconfig/testdata/postreconcile_manifest/test_post_manifest.yaml b/pkg/reconciler/openshift/tektonconfig/testdata/postreconcile_manifest/test_post_manifest.yaml new file mode 100644 index 0000000000..e676ea57ef --- /dev/null +++ b/pkg/reconciler/openshift/tektonconfig/testdata/postreconcile_manifest/test_post_manifest.yaml @@ -0,0 +1,126 @@ +# Copyright 2023 The Tekton Authors +# +# 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. + +--- +apiVersion: v1 +kind: Service +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + labels: + app.kubernetes.io/part-of: tekton-config +spec: + ports: + - name: 8443-tcp + protocol: TCP + port: 8443 + targetPort: 8443 + selector: + name: pipeline-console-plugin + app: pipeline-console-plugin + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + labels: + app.kubernetes.io/part-of: tekton-config +data: + nginx.conf: | + error_log /dev/stdout warn; + events {} + http { + access_log /dev/stdout; + include /etc/nginx/mime.types; + default_type application/octet-stream; + keepalive_timeout 65; + server { + listen 8443 ssl; + listen [::]:8443 ssl; + ssl_certificate /var/cert/tls.crt; + ssl_certificate_key /var/cert/tls.key; + root /usr/share/nginx/html; + } + } + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pipeline-console-plugin + namespace: openshift-pipelines + labels: + app.kubernetes.io/part-of: tekton-config +spec: + replicas: 1 + selector: + matchLabels: + name: pipeline-console-plugin + app: pipeline-console-plugin + template: + metadata: + labels: + name: pipeline-console-plugin + app: pipeline-console-plugin + app.kubernetes.io/part-of: tekton-config + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + volumes: + - name: pipeline-console-plugin-cert + secret: + secretName: pipeline-console-plugin-cert + defaultMode: 420 + - name: nginx-conf + configMap: + name: pipeline-console-plugin + defaultMode: 420 + containers: + - name: pipeline-console-plugin + image: ghcr.io/openshift-pipelines/console-plugin:main + imagePullPolicy: Always + ports: + - protocol: TCP + containerPort: 8443 + volumeMounts: + - name: pipeline-console-plugin-cert + readOnly: true + mountPath: /var/cert + - name: nginx-conf + readOnly: true + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + +--- +apiVersion: console.openshift.io/v1 +kind: ConsolePlugin +metadata: + name: pipeline-console-plugin + labels: + app.kubernetes.io/part-of: tekton-config +spec: + displayName: Pipeline Console Plugin + backend: + type: Service + service: + name: pipeline-console-plugin + namespace: openshift-pipelines + port: 8443 + basePath: "/" + i18n: + loadType: Preload # options: Preload, Lazy