diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go b/pkg/apis/operator/v1alpha1/tektonconfig_defaults.go index 98f432038..2ef549203 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 fca58a33f..ab1822b87 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 d78162d8f..da867405b 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 1699a54a2..fffa5f02f 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 67878ae3a..f2620c53c 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 95a95c2e1..c627eb215 100644 --- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go @@ -1251,7 +1251,6 @@ func (in *ResultsAPIProperties) DeepCopyInto(out *ResultsAPIProperties) { *out = new(uint) **out = **in } - in.Options.DeepCopyInto(&out.Options) return } diff --git a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go index 759b1535c..55de2d846 100644 --- a/pkg/reconciler/kubernetes/tektonresult/tektonresult.go +++ b/pkg/reconciler/kubernetes/tektonresult/tektonresult.go @@ -18,6 +18,8 @@ package tektonresult import ( "context" + "crypto/rand" + "encoding/base64" "errors" "fmt" @@ -145,12 +147,20 @@ 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 not set then create default DB + if !tr.Spec.IsExternalDB { + if err := r.createDBSecret(ctx, tr); err != nil { + return err + } } + + // Validated TLS Secret for kubernetes platform + if !v1alpha1.IsOpenShiftPlatform() { + if err := r.validateTLSSecretsAreCreated(ctx, tr); err != nil { + return err + } + } + tr.Status.MarkDependenciesInstalled() if err := r.extension.PreReconcile(ctx, tr); err != nil { @@ -314,13 +324,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) validateTLSSecretsAreCreated(ctx context.Context, tr *v1alpha1.TektonResult) 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, TlsSecretName, 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", TlsSecretName)) return err } logger.Error(err) @@ -328,3 +338,60 @@ 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 +} + +// 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 +} diff --git a/pkg/reconciler/shared/tektonconfig/result/result.go b/pkg/reconciler/shared/tektonconfig/result/result.go new file mode 100644 index 000000000..dc52eba95 --- /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 000000000..0cb3a5ab6 --- /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 fb7d2e43b..5718c30fc 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" @@ -185,6 +186,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)