diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go b/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go index 98f4320381..2ef549203b 100644 --- a/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go +++ b/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go @@ -32,6 +32,7 @@ func (tc *TektonConfig) SetDefaults(ctx context.Context) { tc.Spec.Pipeline.setDefaults() tc.Spec.Trigger.setDefaults() tc.Spec.Chain.setDefaults() + tc.Spec.Result.setDefaults() if IsOpenShiftPlatform() { if tc.Spec.Platforms.OpenShift.PipelinesAsCode == nil { diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_types.go b/pkg/apis/operator/v1alpha1/tektonconfig_types.go index fca58a33f3..ab1822b878 100644 --- a/pkg/apis/operator/v1alpha1/tektonconfig_types.go +++ b/pkg/apis/operator/v1alpha1/tektonconfig_types.go @@ -105,6 +105,9 @@ type TektonConfigSpec struct { // Chain holds the customizable option for chains component // +optional Chain Chain `json:"chain,omitempty"` + // Result holds the customize option for results component + // +optional + Result Result `json:"result,omitempty"` // Dashboard holds the customizable options for dashboards component // +optional Dashboard Dashboard `json:"dashboard,omitempty"` diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_validation.go b/pkg/apis/operator/v1alpha1/tektonconfig_validation.go index d78162d8f7..da867405b6 100644 --- a/pkg/apis/operator/v1alpha1/tektonconfig_validation.go +++ b/pkg/apis/operator/v1alpha1/tektonconfig_validation.go @@ -120,6 +120,7 @@ func (tc *TektonConfig) Validate(ctx context.Context) (errs *apis.FieldError) { errs = errs.Also(tc.Spec.Dashboard.Options.validate("spec.dashboard.options")) errs = errs.Also(tc.Spec.Chain.Options.validate("spec.chain.options")) errs = errs.Also(tc.Spec.Trigger.Options.validate("spec.trigger.options")) + errs = errs.Also(tc.Spec.Result.Options.validate("spec.result.options")) return errs.Also(tc.Spec.Trigger.TriggersProperties.validate("spec.trigger")) } diff --git a/pkg/apis/operator/v1alpha1/tektonresult_defaults.go b/pkg/apis/operator/v1alpha1/tektonresult_defaults.go index 1699a54a21..b621d87350 100644 --- a/pkg/apis/operator/v1alpha1/tektonresult_defaults.go +++ b/pkg/apis/operator/v1alpha1/tektonresult_defaults.go @@ -26,3 +26,8 @@ func (tp *TektonResult) SetDefaults(ctx context.Context) { tp.Spec.TLSHostnameOverride = "" } } + +// Sets default values of Result +func (c *Result) setDefaults() { + // TODO: Set the other default values for Result +} diff --git a/pkg/apis/operator/v1alpha1/tektonresult_types.go b/pkg/apis/operator/v1alpha1/tektonresult_types.go index 67878ae3a8..f2620c53c6 100644 --- a/pkg/apis/operator/v1alpha1/tektonresult_types.go +++ b/pkg/apis/operator/v1alpha1/tektonresult_types.go @@ -61,6 +61,15 @@ type LokiStackProperties struct { LokiStackNamespace string `json:"loki_stack_namespace,omitempty"` } +// Result defines the field to customize Result component +type Result struct { + // enable or disable Result Component + Disabled bool `json:"disabled"` + TektonResultSpec `json:",inline"` + // Options holds additions fields and these fields will be updated on the manifests + Options AdditionalOptions `json:"options"` +} + // ResultsAPIProperties defines the fields which are configurable for // Results API server config type ResultsAPIProperties struct { diff --git a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go index 95a95c2e10..651882a170 100644 --- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go @@ -1193,6 +1193,24 @@ func (in *ResolversConfig) DeepCopy() *ResolversConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Result) DeepCopyInto(out *Result) { + *out = *in + in.TektonResultSpec.DeepCopyInto(&out.TektonResultSpec) + in.Options.DeepCopyInto(&out.Options) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Result. +func (in *Result) DeepCopy() *Result { + if in == nil { + return nil + } + out := new(Result) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResultsAPIProperties) DeepCopyInto(out *ResultsAPIProperties) { *out = *in @@ -1575,6 +1593,7 @@ func (in *TektonConfigSpec) DeepCopyInto(out *TektonConfigSpec) { in.Pipeline.DeepCopyInto(&out.Pipeline) in.Trigger.DeepCopyInto(&out.Trigger) in.Chain.DeepCopyInto(&out.Chain) + in.Result.DeepCopyInto(&out.Result) in.Dashboard.DeepCopyInto(&out.Dashboard) if in.Params != nil { in, out := &in.Params, &out.Params diff --git a/pkg/reconciler/kubernetes/tektoninstallerset/client/list.go b/pkg/reconciler/kubernetes/tektoninstallerset/client/list.go index 79ab9536b8..94df1b70a0 100644 --- a/pkg/reconciler/kubernetes/tektoninstallerset/client/list.go +++ b/pkg/reconciler/kubernetes/tektoninstallerset/client/list.go @@ -37,3 +37,18 @@ func (i *InstallerSetClient) ListCustomSet(ctx context.Context, labelSelector st } return is, nil } + +// ListPreSet return the lists of Pre sets with the provided labelSelector +func (i *InstallerSetClient) ListPreSet(ctx context.Context, labelSelector string) (*v1alpha1.TektonInstallerSetList, error) { + logger := logging.FromContext(ctx) + logger.Debugf("%v: checking installer sets with labels: %v", i.resourceKind, labelSelector) + + is, err := i.clientSet.List(ctx, v1.ListOptions{LabelSelector: labelSelector}) + if err != nil { + return nil, err + } + if len(is.Items) == 0 { + logger.Debugf("%v: no installer sets found with labels: %v", i.resourceKind, labelSelector) + } + return is, nil +} diff --git a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go index 759b1535c4..1940884509 100644 --- a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go +++ b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go @@ -18,8 +18,19 @@ package tektonresult import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" "errors" "fmt" + "log" + "math/big" + "os" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -145,12 +156,24 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tr *v1alpha1.TektonResul return errors.New(errMsg) } - // check if the secrets are created - // TODO: Create secret automatically if they don't exist - // TODO: And remove this check in future release. - if err := r.validateSecretsAreCreated(ctx, tr); err != nil { - return err + // If external database is disable then create default database and tls secret + // otherwise checks database and secret are created or not + if !tr.Spec.IsExternalDB { + if err := r.createDBSecret(ctx, tr); err != nil { + return err + } + if err := r.createTLSSecret(ctx, tr); err != nil { + return err + } + } else { + if err := r.validateSecretsAreCreated(ctx, tr, DbSecretName); err != nil { + return err + } + if err := r.validateSecretsAreCreated(ctx, tr, TlsSecretName); err != nil { + return err + } } + tr.Status.MarkDependenciesInstalled() if err := r.extension.PreReconcile(ctx, tr); err != nil { @@ -314,13 +337,13 @@ func (r *Reconciler) updateTektonResultsStatus(ctx context.Context, tr *v1alpha1 } // TektonResults expects secrets to be created before installing -func (r *Reconciler) validateSecretsAreCreated(ctx context.Context, tr *v1alpha1.TektonResult) error { +func (r *Reconciler) validateSecretsAreCreated(ctx context.Context, tr *v1alpha1.TektonResult, secretName string) error { logger := logging.FromContext(ctx) - _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Get(ctx, DbSecretName, metav1.GetOptions{}) + _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Get(ctx, secretName, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { logger.Error(err) - tr.Status.MarkDependencyMissing(fmt.Sprintf("%s secret is missing", DbSecretName)) + tr.Status.MarkDependencyMissing(fmt.Sprintf("%s secret is missing", secretName)) return err } logger.Error(err) @@ -328,3 +351,171 @@ func (r *Reconciler) validateSecretsAreCreated(ctx context.Context, tr *v1alpha1 } return nil } + +// Generate the DB secret +func (r *Reconciler) getDBSecret(name string, namespace string, tr *v1alpha1.TektonResult) *corev1.Secret { + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{getOwnerRef(tr)}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{}, + } + password, _ := generateRandomBaseString(20) + s.StringData["POSTGRES_PASSWORD"] = password + s.StringData["POSTGRES_USER"] = "result" + return s +} + +// Create Result default database +func (r *Reconciler) createDBSecret(ctx context.Context, tr *v1alpha1.TektonResult) error { + logger := logging.FromContext(ctx) + + // Get the DB secret, if not found then create the DB secret + _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Get(ctx, DbSecretName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // If not found then create DB secret with default data + newDBSecret := r.getDBSecret(DbSecretName, tr.Spec.TargetNamespace, tr) + _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Create(ctx, newDBSecret, metav1.CreateOptions{}) + if err != nil { + logger.Error(err) + tr.Status.MarkDependencyMissing(fmt.Sprintf("Default db %s creation is failing", DbSecretName)) + return err + } + } + } + return nil +} + +// Create TLS certificates for the database +func (r *Reconciler) createTLSSecret(ctx context.Context, tr *v1alpha1.TektonResult) error { + logger := logging.FromContext(ctx) + + _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Get(ctx, TlsSecretName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + + certPEM, keyPEM, err := generateTLSCertificate() + if err != nil { + logger.Errorf("Error generating TLS certificate: %v", err) + } + err = os.WriteFile("cert.pem", certPEM, 0644) + if err != nil { + logger.Errorf("Error writing cert.pem: %v", err) + } + err = os.WriteFile("key.pem", keyPEM, 0600) + if err != nil { + logger.Errorf("Error writing key.pem: %v", err) + } + + // Create Kubernetes secret + err = r.createKubernetesTLSSecret(ctx, tr.Spec.TargetNamespace, TlsSecretName, certPEM, keyPEM, tr) + if err != nil { + log.Fatalf("Error creating Kubernetes secret: %v", err) + } + + } + logger.Error(err) + return err + } + return nil +} + +// Get an owner reference of Tekton Result +func getOwnerRef(tr *v1alpha1.TektonResult) metav1.OwnerReference { + return *metav1.NewControllerRef(tr, tr.GroupVersionKind()) +} + +func generateRandomBaseString(size int) (string, error) { + bytes := make([]byte, size) + + // Generate random bytes + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + // Encode the random bytes into a Base64 string + base64String := base64.StdEncoding.EncodeToString(bytes) + + return base64String, nil +} + +// generateTLSCertificate generates a self-signed TLS certificate and private key. +func generateTLSCertificate() (certPEM, keyPEM []byte, err error) { + + // Define subject and DNS names + dnsName := fmt.Sprintf("tekton-results-api-service.%s.svc.cluster.local", "tekton-pipelines") + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Issuer: pkix.Name{}, + Subject: pkix.Name{ + CommonName: dnsName, + }, + DNSNames: []string{dnsName}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + + certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + privBytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, nil, err + } + keyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) + + return certPEM, keyPEM, nil +} + +// createKubernetesSecret creates a Kubernetes TLS secret with the given cert and key. +func (r *Reconciler) createKubernetesTLSSecret(ctx context.Context, namespace, secretName string, certPEM, keyPEM []byte, tr *v1alpha1.TektonResult) error { + + // Define the secret + logger := logging.FromContext(ctx) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSCertKey: certPEM, + corev1.TLSPrivateKeyKey: keyPEM, + }, + } + + _, err := r.kubeClientSet.CoreV1().Secrets(tr.Spec.TargetNamespace).Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + logger.Error(err) + tr.Status.MarkDependencyMissing(fmt.Sprintf("Default db %s creation is failing", DbSecretName)) + return err + } + + log.Printf("Secret '%s' created successfully in namespace '%s'\n", secretName, namespace) + return nil +} diff --git a/pkg/reconciler/openshift/tektonresult/extension.go b/pkg/reconciler/openshift/tektonresult/extension.go index e1ea295ebb..d97b0610bb 100644 --- a/pkg/reconciler/openshift/tektonresult/extension.go +++ b/pkg/reconciler/openshift/tektonresult/extension.go @@ -72,7 +72,7 @@ func OpenShiftExtension(ctx context.Context) common.Extension { logger.Fatalf("Failed to fetch logs RBAC manifest: %v", err) } - ext := openshiftExtension{ + ext := &openshiftExtension{ installerSetClient: client.NewInstallerSetClient(operatorclient.Get(ctx).OperatorV1alpha1().TektonInstallerSets(), version, "results-ext", v1alpha1.KindTektonResult, nil), internalDBManifest: internalDBManifest, @@ -87,6 +87,7 @@ type openshiftExtension struct { routeManifest *mf.Manifest internalDBManifest *mf.Manifest logsRBACManifest *mf.Manifest + removePreset bool } func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer { @@ -102,14 +103,20 @@ func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Tr } } -func (oe openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { +func (oe *openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { result := tc.(*v1alpha1.TektonResult) - mf := mf.Manifest{} + if !result.Spec.IsExternalDB { mf = *oe.internalDBManifest + oe.removePreset = true + } + if result.Spec.IsExternalDB && oe.removePreset { + if err := oe.installerSetClient.CleanupPreSet(ctx); err != nil { + return err + } + oe.removePreset = false } - if (result.Spec.LokiStackName != "" && result.Spec.LokiStackNamespace != "") || strings.EqualFold(result.Spec.LogsType, "LOKI") { mf = mf.Append(*oe.logsRBACManifest) diff --git a/pkg/reconciler/shared/tektonconfig/controller.go b/pkg/reconciler/shared/tektonconfig/controller.go index 0f7247dace..450f92d150 100644 --- a/pkg/reconciler/shared/tektonconfig/controller.go +++ b/pkg/reconciler/shared/tektonconfig/controller.go @@ -30,6 +30,7 @@ import ( tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig" tektonInstallerinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektoninstallerset" tektonPipelineinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonpipeline" + tektonResultinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonresult" tektonTriggerinformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektontrigger" tektonConfigreconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonconfig" "github.com/tektoncd/operator/pkg/reconciler/common" @@ -105,6 +106,13 @@ func NewExtensibleController(generator common.ExtensionGenerator) injection.Cont logger.Panicf("Couldn't register TektonChain informer event handler: %w", err) } + if _, err := tektonResultinformer.Get(ctx).Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterController(&v1alpha1.TektonConfig{}), + Handler: controller.HandleAll(impl.EnqueueControllerOf), + }); err != nil { + logger.Panicf("Couldn't register TektonResult informer event handler: %w", err) + } + if _, err := tektonInstallerinformer.Get(ctx).Informer().AddEventHandler(cache.FilteringResourceEventHandler{ FilterFunc: controller.FilterController(&v1alpha1.TektonConfig{}), Handler: controller.HandleAll(impl.EnqueueControllerOf), diff --git a/pkg/reconciler/shared/tektonconfig/result/result.go b/pkg/reconciler/shared/tektonconfig/result/result.go new file mode 100644 index 0000000000..dc52eba958 --- /dev/null +++ b/pkg/reconciler/shared/tektonconfig/result/result.go @@ -0,0 +1,172 @@ +/* +Copyright 2024 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. +*/ + +package result + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" +) + +func EnsureTektonResultExists(ctx context.Context, clients op.TektonResultInterface, tr *v1alpha1.TektonResult) (*v1alpha1.TektonResult, error) { + trCR, err := GetResult(ctx, clients, v1alpha1.ResultResourceName) + if err != nil { + if !apierrs.IsNotFound(err) { + return nil, err + } + if err := CreateResult(ctx, clients, tr); err != nil { + return nil, err + } + return nil, v1alpha1.RECONCILE_AGAIN_ERR + } + + trCR, err = UpdateResult(ctx, trCR, tr, clients) + if err != nil { + return nil, err + } + + ready, err := isTektonResultReady(trCR) + if err != nil { + return nil, err + } + if !ready { + return nil, v1alpha1.RECONCILE_AGAIN_ERR + } + + return trCR, err +} + +func EnsureTektonResultCRNotExists(ctx context.Context, clients op.TektonResultInterface) error { + if _, err := GetResult(ctx, clients, v1alpha1.ResultResourceName); err != nil { + if apierrs.IsNotFound(err) { + // TektonResult CR is gone, hence return nil + return nil + } + return err + } + // if the Get was successful, try deleting the CR + if err := clients.Delete(ctx, v1alpha1.ResultResourceName, metav1.DeleteOptions{}); err != nil { + if apierrs.IsNotFound(err) { + // TektonResult CR is gone, hence return nil + return nil + } + return fmt.Errorf("TektonResult %q failed to delete: %v", v1alpha1.ResultResourceName, err) + } + // if the Delete API call was success, + // then return requeue_event + // so that in a subsequent reconcile call the absence of the CR is verified by one of the 2 checks above + return v1alpha1.RECONCILE_AGAIN_ERR +} + +// Get the result +func GetResult(ctx context.Context, clients op.TektonResultInterface, name string) (*v1alpha1.TektonResult, error) { + return clients.Get(ctx, name, metav1.GetOptions{}) +} + +// Create the Result + +func CreateResult(ctx context.Context, clients op.TektonResultInterface, tr *v1alpha1.TektonResult) error { + _, err := clients.Create(ctx, tr, metav1.CreateOptions{}) + return err +} + +func isTektonResultReady(s *v1alpha1.TektonResult) (bool, error) { + if s.GetStatus() != nil && s.GetStatus().GetCondition(apis.ConditionReady) != nil { + if strings.Contains(s.GetStatus().GetCondition(apis.ConditionReady).Message, v1alpha1.UpgradePending) { + return false, v1alpha1.DEPENDENCY_UPGRADE_PENDING_ERR + } + } + return s.Status.IsReady(), nil +} + +func UpdateResult(ctx context.Context, old *v1alpha1.TektonResult, new *v1alpha1.TektonResult, clients op.TektonResultInterface) (*v1alpha1.TektonResult, error) { + // if the result spec is changed then update the instance + updated := false + + // initialize labels(map) object + if old.ObjectMeta.Labels == nil { + old.ObjectMeta.Labels = map[string]string{} + } + + if new.Spec.TargetNamespace != old.Spec.TargetNamespace { + old.Spec.TargetNamespace = new.Spec.TargetNamespace + updated = true + } + + if !reflect.DeepEqual(old.Spec.ResultsAPIProperties, new.Spec.ResultsAPIProperties) { + old.Spec.ResultsAPIProperties = new.Spec.ResultsAPIProperties + updated = true + } + + if !reflect.DeepEqual(old.Spec.LokiStackProperties, new.Spec.LokiStackProperties) { + old.Spec.LokiStackProperties = new.Spec.LokiStackProperties + updated = true + } + + if !reflect.DeepEqual(old.Spec.ResultsAPIProperties.Options, new.Spec.ResultsAPIProperties.Options) { + old.Spec.ResultsAPIProperties.Options = new.Spec.ResultsAPIProperties.Options + updated = true + } + + if old.ObjectMeta.OwnerReferences == nil { + old.ObjectMeta.OwnerReferences = new.ObjectMeta.OwnerReferences + updated = true + } + + oldLabels, oldHasLabels := old.ObjectMeta.Labels[v1alpha1.ReleaseVersionKey] + newLabels, newHasLabels := new.ObjectMeta.Labels[v1alpha1.ReleaseVersionKey] + if !oldHasLabels || (newHasLabels && oldLabels != newLabels) { + old.ObjectMeta.Labels[v1alpha1.ReleaseVersionKey] = newLabels + updated = true + } + + if updated { + _, err := clients.Update(ctx, old, metav1.UpdateOptions{}) + if err != nil { + return nil, err + } + return nil, v1alpha1.RECONCILE_AGAIN_ERR + } + return old, nil +} + +func GetTektonResultCR(config *v1alpha1.TektonConfig, operatorVersion string) *v1alpha1.TektonResult { + ownerRef := *metav1.NewControllerRef(config, config.GroupVersionKind()) + return &v1alpha1.TektonResult{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1alpha1.ResultResourceName, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + Labels: map[string]string{ + v1alpha1.ReleaseVersionKey: operatorVersion, + }, + }, + Spec: v1alpha1.TektonResultSpec{ + CommonSpec: v1alpha1.CommonSpec{ + TargetNamespace: config.Spec.TargetNamespace, + }, + ResultsAPIProperties: config.Spec.Result.ResultsAPIProperties, + LokiStackProperties: config.Spec.Result.LokiStackProperties, + }, + } +} diff --git a/pkg/reconciler/shared/tektonconfig/result/result_test.go b/pkg/reconciler/shared/tektonconfig/result/result_test.go new file mode 100644 index 0000000000..0cb3a5ab6e --- /dev/null +++ b/pkg/reconciler/shared/tektonconfig/result/result_test.go @@ -0,0 +1,141 @@ +/* +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. +*/ +package result + +import ( + "context" + "testing" + + op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + + "github.com/tektoncd/operator/pkg/client/injection/client/fake" + util "github.com/tektoncd/operator/pkg/reconciler/common/testing" + ts "knative.dev/pkg/reconciler/testing" +) + +func TestEnsureTektonResultExists(t *testing.T) { + ctx, _, _ := ts.SetupFakeContextWithCancel(t) + c := fake.Get(ctx) + tt := GetTektonResultCR(getTektonConfig(), "v0.70.0") + + // first invocation should create instance as it is non-existent and return RECONCILE_AGAIN_ERR + _, err := EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + // during second invocation instance exists but waiting on dependencies (pipeline, results) + // hence returns RECONCILE_AGAIN_ERR + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + // make upgrade checks pass + makeUpgradeCheckPass(t, ctx, c.OperatorV1alpha1().TektonResults()) + + // next invocation should return RECONCILE_AGAIN_ERR as Dashboard is waiting for installation (prereconcile, postreconcile, installersets...) + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + // mark the instance ready + markResultReady(t, ctx, c.OperatorV1alpha1().TektonResults()) + + // next invocation should return nil error as the instance is ready + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, nil) + + // test update propagation from tektonConfig + tt.Spec.TargetNamespace = "foobar" + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, nil) +} + +func TestEnsureTektonResultCRNotExists(t *testing.T) { + ctx, _, _ := ts.SetupFakeContextWithCancel(t) + c := fake.Get(ctx) + + // when no instance exists, nil error is returned immediately + err := EnsureTektonResultCRNotExists(ctx, c.OperatorV1alpha1().TektonResults()) + util.AssertEqual(t, err, nil) + + // create an instance for testing other cases + tt := GetTektonResultCR(getTektonConfig(), "v0.70.0") + _, err = EnsureTektonResultExists(ctx, c.OperatorV1alpha1().TektonResults(), tt) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + // when an instance exists the first invocation should make the delete API call and + // return RECONCILE_AGAIN_ERROR. So that the deletion can be confirmed in a subsequent invocation + err = EnsureTektonResultCRNotExists(ctx, c.OperatorV1alpha1().TektonResults()) + util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) + + // when the instance is completely removed from a cluster, the function should return nil error + err = EnsureTektonResultCRNotExists(ctx, c.OperatorV1alpha1().TektonResults()) + util.AssertEqual(t, err, nil) +} + +func markResultReady(t *testing.T, ctx context.Context, c op.TektonResultInterface) { + t.Helper() + tr, err := c.Get(ctx, v1alpha1.ResultResourceName, metav1.GetOptions{}) + util.AssertEqual(t, err, nil) + tr.Status.MarkDependenciesInstalled() + tr.Status.MarkPreReconcilerComplete() + tr.Status.MarkInstallerSetAvailable() + tr.Status.MarkInstallerSetReady() + tr.Status.MarkPostReconcilerComplete() + _, err = c.UpdateStatus(ctx, tr, metav1.UpdateOptions{}) + util.AssertEqual(t, err, nil) +} + +func makeUpgradeCheckPass(t *testing.T, ctx context.Context, c op.TektonResultInterface) { + t.Helper() + // set necessary version labels to make upgrade check pass + result, err := c.Get(ctx, v1alpha1.ResultResourceName, metav1.GetOptions{}) + util.AssertEqual(t, err, nil) + setDummyVersionLabel(t, result) + _, err = c.Update(ctx, result, metav1.UpdateOptions{}) + util.AssertEqual(t, err, nil) +} + +func setDummyVersionLabel(t *testing.T, tr *v1alpha1.TektonResult) { + t.Helper() + + oprVersion := "v1.2.3" + t.Setenv(v1alpha1.VersionEnvKey, oprVersion) + + labels := tr.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[v1alpha1.ReleaseVersionKey] = oprVersion + tr.SetLabels(labels) +} + +func getTektonConfig() *v1alpha1.TektonConfig { + return &v1alpha1.TektonConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: v1alpha1.ConfigResourceName, + }, + Spec: v1alpha1.TektonConfigSpec{ + Profile: v1alpha1.ProfileAll, + CommonSpec: v1alpha1.CommonSpec{ + TargetNamespace: "tekton-pipelines", + }, + }, + } +} diff --git a/pkg/reconciler/shared/tektonconfig/tektonconfig.go b/pkg/reconciler/shared/tektonconfig/tektonconfig.go index fb7d2e43bf..e125afc498 100644 --- a/pkg/reconciler/shared/tektonconfig/tektonconfig.go +++ b/pkg/reconciler/shared/tektonconfig/tektonconfig.go @@ -27,6 +27,7 @@ import ( "github.com/tektoncd/operator/pkg/reconciler/common" "github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/chain" "github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/pipeline" + "github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/result" "github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/trigger" "github.com/tektoncd/operator/pkg/reconciler/shared/tektonconfig/upgrade" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -73,6 +74,9 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, original *v1alpha1.Tekton if err := chain.EnsureTektonChainCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonChains()); err != nil { return err } + if err := result.EnsureTektonResultCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonResults()); err != nil { + return err + } if err := pipeline.EnsureTektonPipelineCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonPipelines()); err != nil { return err } @@ -185,6 +189,20 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi } } + // Create Results CR if it's enable + if !tc.Spec.Result.Disabled { + tektonresult := result.GetTektonResultCR(tc, r.operatorVersion) + if _, err = result.EnsureTektonResultExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonResults(), tektonresult); err != nil { + tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonResult %s", err.Error())) + return v1alpha1.REQUEUE_EVENT_AFTER + } + } else { + if err := result.EnsureTektonResultCRNotExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonResults()); err != nil { + tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonResult: %s", err.Error())) + return v1alpha1.REQUEUE_EVENT_AFTER + } + } + // reconcile pruner installerSet if !tc.Spec.Pruner.Disabled { err := r.reconcilePrunerInstallerSet(ctx, tc)