diff --git a/cmd/resolvers/main.go b/cmd/resolvers/main.go index 6de254f5b85..cd734b1f9a3 100644 --- a/cmd/resolvers/main.go +++ b/cmd/resolvers/main.go @@ -23,6 +23,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/resolution/v1alpha1" "github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/cluster" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "github.com/tektoncd/pipeline/pkg/resolution/resolver/git" "github.com/tektoncd/pipeline/pkg/resolution/resolver/hub" @@ -49,5 +50,6 @@ func main() { sharedmain.MainWithContext(ctx, "controller", framework.NewController(ctx, &git.Resolver{}), framework.NewController(ctx, &hub.Resolver{HubURL: hubURL}), - framework.NewController(ctx, &bundle.Resolver{})) + framework.NewController(ctx, &bundle.Resolver{}), + framework.NewController(ctx, &cluster.Resolver{})) } diff --git a/config/config-feature-flags.yaml b/config/config-feature-flags.yaml index 59e7e72623e..d97e49bfc5c 100644 --- a/config/config-feature-flags.yaml +++ b/config/config-feature-flags.yaml @@ -93,3 +93,7 @@ data: # This is an experimental feature and thus should still be considered # an alpha feature. enable-git-resolver: "false" + # Setting this flag to "true" enables remote resolution of tasks and pipelines from other namespaces within the cluster. + # This is an experimental feature and thus should still be considered + # an alpha feature. + enable-cluster-resolver: "false" diff --git a/config/resolvers/200-clusterrole.yaml b/config/resolvers/200-clusterrole.yaml index 525dcaa90f2..e338061b55b 100644 --- a/config/resolvers/200-clusterrole.yaml +++ b/config/resolvers/200-clusterrole.yaml @@ -25,3 +25,6 @@ rules: - apiGroups: ["resolution.tekton.dev"] resources: ["resolutionrequests", "resolutionrequests/status"] verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: ["tekton.dev"] + resources: ["tasks", "pipelines"] + verbs: ["get", "list"] diff --git a/config/resolvers/cluster-resolver-config.yaml b/config/resolvers/cluster-resolver-config.yaml new file mode 100644 index 00000000000..7e3cd734678 --- /dev/null +++ b/config/resolvers/cluster-resolver-config.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 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 +# +# https://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: ConfigMap +metadata: + name: cluster-resolver-config + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +data: + # The default kind to fetch. + default-kind: "task" diff --git a/docs/cluster-resolver.md b/docs/cluster-resolver.md new file mode 100644 index 00000000000..55154a68731 --- /dev/null +++ b/docs/cluster-resolver.md @@ -0,0 +1,78 @@ +# Cluster Resolver + +## Resolver Type + +This Resolver responds to type `cluster`. + +## Parameters + +| Param Name | Description | Example Value | +|-------------|-------------------------------------------------------|------------------------------| +| `kind` | The kind of resource to fetch. | `task`, `pipeline` | +| `name` | The name of the resource to fetch. | `some-pipeline`, `some-task` | +| `namespace` | The namespace in the cluster containing the resource. | `default`, `other-namespace` | + +## Requirements + +- A cluster running Tekton Pipeline v0.40.0 or later, with the `alpha` feature gate enabled. +- The [built-in remote resolvers installed](./install.md#installing-and-configuring-remote-task-and-pipeline-resolution). +- The `enable-cluster-resolver` feature flag set to `true`. + +## Configuration + +This resolver uses a `ConfigMap` for its settings. See +[`../config/resolvers/cluster-resolver-config.yaml`](../config/resolvers/cluster-resolver-config.yaml) +for the name, namespace and defaults that the resolver ships with. + +### Options + +| Option Name | Description | Example Values | +|----------------|--------------------------------------------------------------------|--------------------| +| `default-kind` | The default resource kind to fetch if not specified in parameters. | `task`, `pipeline` | + +## Usage + +### Task Resolution + +```yaml +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: remote-task-reference +spec: + taskRef: + resolver: cluster + params: + - name: kind + value: task + - name: name + value: some-task + - name: namespace + value: namespace-containing-task +``` + +### Pipeline resolution + +```yaml +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: remote-pipeline-reference +spec: + pipelineRef: + resolver: cluster + params: + - name: kind + value: pipeline + - name: name + value: some-pipeline + - name: namespace + value: namespace-containing-pipeline +``` + +--- + +Except as otherwise noted, the content of this page is licensed under the +[Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), +and code samples are licensed under the +[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/docs/install.md b/docs/install.md index d8898fd6118..701bdce9440 100644 --- a/docs/install.md +++ b/docs/install.md @@ -293,6 +293,8 @@ By default, these remote resolvers are disabled. Each resolver is enabled by set feature flag to `true`. 1. [The `hub` resolver](./hub-resolver.md), enabled by setting the `enable-hub-resolver` feature flag to `true`. +1. [The `cluster` resolver](./cluster-resolver.md), enabled by setting the `enable-cluster-resolver` + feature flag to `true`. ## Configuring CloudEvents notifications diff --git a/docs/resolution-getting-started.md b/docs/resolution-getting-started.md index eaf8f6695df..d8aec6b265c 100644 --- a/docs/resolution-getting-started.md +++ b/docs/resolution-getting-started.md @@ -40,6 +40,7 @@ The feature flags for the built-in resolvers are: * The `bundles` resolver: `enable-bundles-resolver` * The `git` resolver: `enable-git-resolver` * The `hub` resolver: `enable-hub-resolver` +* The `cluster` resolver: `enable-cluster-resolver` ## Step 3: Try it out! diff --git a/docs/resolution.md b/docs/resolution.md index 07d8851dc9b..d0622fef50c 100644 --- a/docs/resolution.md +++ b/docs/resolution.md @@ -13,6 +13,8 @@ For new users getting started with Tekton Pipeilne remote resolution, check out feature flag to `true`. 1. [The `hub` resolver](./hub-resolver.md), enabled by setting the `enable-hub-resolver` feature flag to `true`. +1. [The `cluster` resolver](./cluster-resolver.md), enabled by setting the `enable-cluster-resolver` + feature flag to `true`. ## Developer Howto: Writing a Resolver From Scratch diff --git a/pkg/apis/config/feature_flags.go b/pkg/apis/config/feature_flags.go index 3011c6c6b57..35a7f46cc9e 100644 --- a/pkg/apis/config/feature_flags.go +++ b/pkg/apis/config/feature_flags.go @@ -66,6 +66,8 @@ const ( DefaultEnableHubResolver = false // DefaultEnableBundlesResolver is the default value for "enable-bundles-resolver". DefaultEnableBundlesResolver = false + // DefaultEnableClusterResolver is the default value for "enable-cluster-resolver". + DefaultEnableClusterResolver = false disableAffinityAssistantKey = "disable-affinity-assistant" disableCredsInitKey = "disable-creds-init" @@ -84,6 +86,8 @@ const ( EnableHubResolver = "enable-hub-resolver" // EnableBundlesResolver is the flag used to enable the bundle remote resolver EnableBundlesResolver = "enable-bundles-resolver" + // EnableClusterResolver is the flag used to enable the cluster remote resolver + EnableClusterResolver = "enable-cluster-resolver" ) // FeatureFlags holds the features configurations @@ -103,6 +107,7 @@ type FeatureFlags struct { EnableGitResolver bool EnableHubResolver bool EnableBundleResolver bool + EnableClusterResolver bool } // GetFeatureFlagsConfigName returns the name of the configmap containing all @@ -163,6 +168,9 @@ func NewFeatureFlagsFromMap(cfgMap map[string]string) (*FeatureFlags, error) { if err := setFeature(EnableBundlesResolver, DefaultEnableBundlesResolver, &tc.EnableBundleResolver); err != nil { return nil, err } + if err := setFeature(EnableClusterResolver, DefaultEnableClusterResolver, &tc.EnableClusterResolver); err != nil { + return nil, err + } // Given that they are alpha features, Tekton Bundles and Custom Tasks should be switched on if // enable-api-fields is "alpha". If enable-api-fields is not "alpha" then fall back to the value of diff --git a/pkg/resolution/resolver/cluster/annotations.go b/pkg/resolution/resolver/cluster/annotations.go new file mode 100644 index 00000000000..3951e496583 --- /dev/null +++ b/pkg/resolution/resolver/cluster/annotations.go @@ -0,0 +1,25 @@ +/* + Copyright 2022 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 cluster + +const ( + // ResourceNameAnnotation is the annotation key for the fetched resource name + ResourceNameAnnotation = "name" + // ResourceNamespaceAnnotation is the annotation key for the fetched resource's namespace + ResourceNamespaceAnnotation = "namespace" +) diff --git a/pkg/resolution/resolver/cluster/config.go b/pkg/resolution/resolver/cluster/config.go new file mode 100644 index 00000000000..d000af0046d --- /dev/null +++ b/pkg/resolution/resolver/cluster/config.go @@ -0,0 +1,22 @@ +/* +Copyright 2022 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 cluster + +const ( + // DefaultKindKey is the key in the config map for the default kind setting + DefaultKindKey = "default-kind" +) diff --git a/pkg/resolution/resolver/cluster/params.go b/pkg/resolution/resolver/cluster/params.go new file mode 100644 index 00000000000..902d770f176 --- /dev/null +++ b/pkg/resolution/resolver/cluster/params.go @@ -0,0 +1,26 @@ +/* +Copyright 2022 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 cluster + +const ( + // KindParam is the parameter for the object kind + KindParam = "kind" + // NameParam is the parameter for the object name + NameParam = "name" + // NamespaceParam is the parameter for the namespace containing the object + NamespaceParam = "namespace" +) diff --git a/pkg/resolution/resolver/cluster/resolver.go b/pkg/resolution/resolver/cluster/resolver.go new file mode 100644 index 00000000000..9814a5dfa8a --- /dev/null +++ b/pkg/resolution/resolver/cluster/resolver.go @@ -0,0 +1,211 @@ +/* +Copyright 2022 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 cluster + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/tektoncd/pipeline/pkg/apis/config" + clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/logging" + "sigs.k8s.io/yaml" +) + +const ( + disabledError = "cannot handle resolution request, enable-cluster-resolver feature flag not true" + + // LabelValueClusterResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueClusterResolverType string = "cluster" + + // ClusterResolverName is the name that the cluster resolver should be + // associated with + ClusterResolverName string = "Cluster" + + configMapName = "cluster-resolver-config" +) + +var _ framework.Resolver = &Resolver{} + +// Resolver implements a framework.Resolver that can fetch resources from other namespaces. +type Resolver struct { + pipelineClientSet clientset.Interface +} + +// Initialize performs any setup required by the cluster resolver. +func (r *Resolver) Initialize(ctx context.Context) error { + r.pipelineClientSet = pipelineclient.Get(ctx) + return nil +} + +// GetName returns the string name that the cluster resolver should be +// associated with. +func (r *Resolver) GetName(_ context.Context) string { + return ClusterResolverName +} + +// GetSelector returns the labels that resource requests are required to have for +// the cluster resolver to process them. +func (r *Resolver) GetSelector(_ context.Context) map[string]string { + return map[string]string{ + resolutioncommon.LabelKeyResolverType: LabelValueClusterResolverType, + } +} + +// ValidateParams returns an error if the given parameter map is not +// valid for a resource request targeting the cluster resolver. +func (r *Resolver) ValidateParams(ctx context.Context, params map[string]string) error { + if r.isDisabled(ctx) { + return errors.New(disabledError) + } + + _, err := populateParamsWithDefaults(ctx, params) + return err +} + +// Resolve performs the work of fetching a resource from a namespace with the given +// parameters. +func (r *Resolver) Resolve(ctx context.Context, origParams map[string]string) (framework.ResolvedResource, error) { + if r.isDisabled(ctx) { + return nil, errors.New(disabledError) + } + + logger := logging.FromContext(ctx) + + params, err := populateParamsWithDefaults(ctx, origParams) + if err != nil { + logger.Infof("cluster resolver parameter(s) invalid: %v", err) + return nil, err + } + + var data []byte + + switch params[KindParam] { + case "task": + task, err := r.pipelineClientSet.TektonV1beta1().Tasks(params[NamespaceParam]).Get(ctx, params[NameParam], metav1.GetOptions{}) + if err != nil { + logger.Infof("failed to load task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } + task.Kind = "Task" + task.APIVersion = "tekton.dev/v1beta1" + data, err = yaml.Marshal(task) + if err != nil { + logger.Infof("failed to marshal task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } + case "pipeline": + pipeline, err := r.pipelineClientSet.TektonV1beta1().Pipelines(params[NamespaceParam]).Get(ctx, params[NameParam], metav1.GetOptions{}) + if err != nil { + logger.Infof("failed to load pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } + pipeline.Kind = "Pipeline" + pipeline.APIVersion = "tekton.dev/v1beta1" + data, err = yaml.Marshal(pipeline) + if err != nil { + logger.Infof("failed to marshal pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return nil, err + } + default: + logger.Infof("unknown or invalid resource kind %s", params[KindParam]) + return nil, fmt.Errorf("unknown or invalid resource kind %s", params[KindParam]) + } + + return &ResolvedClusterResource{ + Content: data, + Name: params[NameParam], + Namespace: params[NamespaceParam], + }, nil +} + +var _ framework.ConfigWatcher = &Resolver{} + +// GetConfigName returns the name of the cluster resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return configMapName +} + +func (r *Resolver) isDisabled(ctx context.Context) bool { + cfg := config.FromContextOrDefaults(ctx) + if cfg.FeatureFlags.EnableClusterResolver { + return false + } + + return true +} + +// ResolvedClusterResource implements framework.ResolvedResource and returns +// the resolved file []byte data and an annotation map for any metadata. +type ResolvedClusterResource struct { + Content []byte + Name string + Namespace string +} + +var _ framework.ResolvedResource = &ResolvedClusterResource{} + +// Data returns the bytes of the file resolved from git. +func (r *ResolvedClusterResource) Data() []byte { + return r.Content +} + +// Annotations returns the metadata that accompanies the resource fetched from the cluster. +func (r *ResolvedClusterResource) Annotations() map[string]string { + return map[string]string{ + ResourceNameAnnotation: r.Name, + ResourceNamespaceAnnotation: r.Namespace, + } +} + +func populateParamsWithDefaults(ctx context.Context, params map[string]string) (map[string]string, error) { + conf := framework.GetResolverConfigFromContext(ctx) + + var missingParams []string + + if _, ok := params[KindParam]; !ok { + if kindVal, ok := conf[DefaultKindKey]; !ok { + missingParams = append(missingParams, KindParam) + } else { + params[KindParam] = kindVal + } + } + if kindVal, ok := params[KindParam]; ok && kindVal != "task" && kindVal != "pipeline" { + return nil, fmt.Errorf("unknown or unsupported resource kind '%s'", kindVal) + } + + if _, ok := params[NameParam]; !ok { + missingParams = append(missingParams, NameParam) + } + + if _, ok := params[NamespaceParam]; !ok { + missingParams = append(missingParams, NamespaceParam) + } + + if len(missingParams) > 0 { + return nil, fmt.Errorf("missing required cluster resolver params: %s", strings.Join(missingParams, ", ")) + } + + return params, nil +} diff --git a/pkg/resolution/resolver/cluster/resolver_test.go b/pkg/resolution/resolver/cluster/resolver_test.go new file mode 100644 index 00000000000..a1af790c8ad --- /dev/null +++ b/pkg/resolution/resolver/cluster/resolver_test.go @@ -0,0 +1,305 @@ +/* + Copyright 2022 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 cluster + +import ( + "context" + "encoding/base64" + "errors" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/config" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1alpha1" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" + "github.com/tektoncd/pipeline/test" + "github.com/tektoncd/pipeline/test/diff" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/system" + "sigs.k8s.io/yaml" + + _ "knative.dev/pkg/system/testing" +) + +func TestGetSelector(t *testing.T) { + resolver := Resolver{} + sel := resolver.GetSelector(resolverContext()) + if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != LabelValueClusterResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidateParams(t *testing.T) { + resolver := Resolver{} + + params := map[string]string{ + KindParam: "task", + NamespaceParam: "foo", + NameParam: "baz", + } + + if err := resolver.ValidateParams(resolverContext(), params); err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } +} + +func TestValidateParamsNotEnabled(t *testing.T) { + resolver := Resolver{} + + var err error + + params := map[string]string{ + KindParam: "task", + NamespaceParam: "foo", + NameParam: "baz", + } + err = resolver.ValidateParams(context.Background(), params) + if err == nil { + t.Fatalf("expected disabled err") + } + if d := cmp.Diff(disabledError, err.Error()); d != "" { + t.Errorf("unexpected error: %s", diff.PrintWantGot(d)) + } +} + +func TestValidateParamsFailure(t *testing.T) { + testCases := []struct { + name string + params map[string]string + expectedErr string + }{ + { + name: "missing kind", + params: map[string]string{ + NameParam: "foo", + NamespaceParam: "bar", + }, + expectedErr: "missing required cluster resolver params: kind", + }, { + name: "invalid kind", + params: map[string]string{ + KindParam: "banana", + NamespaceParam: "foo", + NameParam: "bar", + }, + expectedErr: "unknown or unsupported resource kind 'banana'", + }, { + name: "missing multiple", + params: map[string]string{ + KindParam: "task", + }, + expectedErr: "missing required cluster resolver params: name, namespace", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := &Resolver{} + err := resolver.ValidateParams(resolverContext(), tc.params) + if err == nil { + t.Fatalf("got no error, but expected: %s", tc.expectedErr) + } + if d := cmp.Diff(tc.expectedErr, err.Error()); d != "" { + t.Errorf("error did not match: %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestResolve(t *testing.T) { + exampleTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-task", + Namespace: "task-ns", + ResourceVersion: "00002", + }, + TypeMeta: metav1.TypeMeta{ + Kind: string(v1beta1.NamespacedTaskKind), + APIVersion: "tekton.dev/v1beta1", + }, + Spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "some-step", + Image: "some-image", + Command: []string{"something"}, + }}, + }, + } + taskAsYAML, err := yaml.Marshal(exampleTask) + if err != nil { + t.Fatalf("couldn't marshal task: %v", err) + } + + examplePipeline := &v1beta1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pipeline", + Namespace: "pipeline-ns", + ResourceVersion: "00001", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Pipeline", + APIVersion: "tekton.dev/v1beta1", + }, + Spec: v1beta1.PipelineSpec{ + Tasks: []v1beta1.PipelineTask{{ + Name: "some-pipeline-task", + TaskRef: &v1beta1.TaskRef{ + Name: "some-task", + Kind: v1beta1.NamespacedTaskKind, + }, + }}, + }, + } + pipelineAsYAML, err := yaml.Marshal(examplePipeline) + if err != nil { + t.Fatalf("couldn't marshal pipeline: %v", err) + } + + testCases := []struct { + name string + kind string + resourceName string + namespace string + expectedStatus *v1alpha1.ResolutionRequestStatus + expectedErr error + }{ + { + name: "successful task", + kind: "task", + resourceName: exampleTask.Name, + namespace: exampleTask.Namespace, + expectedStatus: &v1alpha1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1alpha1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(taskAsYAML), + }, + }, + }, { + name: "successful pipeline", + kind: "pipeline", + resourceName: examplePipeline.Name, + namespace: examplePipeline.Namespace, + expectedStatus: &v1alpha1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1alpha1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(pipelineAsYAML), + }, + }, + }, { + name: "no such task", + kind: "task", + resourceName: exampleTask.Name, + namespace: "other-ns", + expectedStatus: &v1alpha1.ResolutionRequestStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionFalse, + Reason: resolutioncommon.ReasonResolutionFailed, + }}, + }, + }, + expectedErr: errors.New(`error getting "Cluster" "foo/rr": tasks.tekton.dev "example-task" not found`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + + request := createRequest(tc.kind, tc.resourceName, tc.namespace) + + d := test.Data{ + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: system.Namespace(), + }, + Data: map[string]string{ + DefaultKindKey: "task", + }, + }, { + ObjectMeta: metav1.ObjectMeta{Namespace: system.Namespace(), Name: config.GetFeatureFlagsConfigName()}, + Data: map[string]string{ + "enable-cluster-resolver": "true", + }, + }}, + Pipelines: []*v1beta1.Pipeline{examplePipeline}, + ResolutionRequests: []*v1alpha1.ResolutionRequest{request}, + Tasks: []*v1beta1.Task{exampleTask}, + } + + resolver := &Resolver{} + + var expectedStatus *v1alpha1.ResolutionRequestStatus + if tc.expectedStatus != nil { + expectedStatus = tc.expectedStatus.DeepCopy() + + if tc.expectedErr == nil { + reqParams := request.Spec.Parameters + if expectedStatus.Annotations == nil { + expectedStatus.Annotations = make(map[string]string) + } + expectedStatus.Annotations[ResourceNameAnnotation] = reqParams[NameParam] + expectedStatus.Annotations[ResourceNamespaceAnnotation] = reqParams[NamespaceParam] + } else { + expectedStatus.Status.Conditions[0].Message = tc.expectedErr.Error() + } + } + + frtesting.RunResolverReconcileTest(ctx, t, d, resolver, request, expectedStatus, tc.expectedErr) + }) + } +} + +func createRequest(kind, name, namespace string) *v1alpha1.ResolutionRequest { + return &v1alpha1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1alpha1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: LabelValueClusterResolverType, + }, + }, + Spec: v1alpha1.ResolutionRequestSpec{ + Parameters: map[string]string{ + KindParam: kind, + NameParam: name, + NamespaceParam: namespace, + }, + }, + } +} + +func resolverContext() context.Context { + return frtesting.ContextWithClusterResolverEnabled(context.Background()) +} diff --git a/pkg/resolution/resolver/framework/testing/fakecontroller.go b/pkg/resolution/resolver/framework/testing/fakecontroller.go index eb1be824564..d1552547f4a 100644 --- a/pkg/resolution/resolver/framework/testing/fakecontroller.go +++ b/pkg/resolution/resolver/framework/testing/fakecontroller.go @@ -54,12 +54,16 @@ var ( // reconciles the given request. It then checks for the expected error, if any, and compares the resulting status with // the expected status. func RunResolverReconcileTest(ctx context.Context, t *testing.T, d test.Data, resolver framework.Resolver, request *v1alpha1.ResolutionRequest, - expectedStatus *v1alpha1.ResolutionRequestStatus, expectedErr error) { + expectedStatus *v1alpha1.ResolutionRequestStatus, expectedErr error, resolverModifiers ...func(resolver framework.Resolver, testAssets test.Assets)) { t.Helper() testAssets, cancel := GetResolverFrameworkController(ctx, t, d, resolver, setClockOnReconciler) defer cancel() + for _, rm := range resolverModifiers { + rm(resolver, testAssets) + } + err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRequestName(request)) if expectedErr != nil { if err == nil { diff --git a/pkg/resolution/resolver/framework/testing/featureflag.go b/pkg/resolution/resolver/framework/testing/featureflag.go index bb085465064..5a7cfbe5818 100644 --- a/pkg/resolution/resolver/framework/testing/featureflag.go +++ b/pkg/resolution/resolver/framework/testing/featureflag.go @@ -38,6 +38,11 @@ func ContextWithBundlesResolverEnabled(ctx context.Context) context.Context { return contextWithResolverEnabled(ctx, "enable-bundles-resolver") } +// ContextWithClusterResolverEnabled returns a context containing a Config with the enable-cluster-resolver feature flag enabled. +func ContextWithClusterResolverEnabled(ctx context.Context) context.Context { + return contextWithResolverEnabled(ctx, "enable-cluster-resolver") +} + func contextWithResolverEnabled(ctx context.Context, resolverFlag string) context.Context { featureFlags, _ := config.NewFeatureFlagsFromMap(map[string]string{ resolverFlag: "true", diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index 9ed533d25f8..04eacafc989 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -52,7 +52,7 @@ function set_feature_gate() { resolver="true" fi printf "Setting feature gate to %s\n", ${gate} - jsonpatch=$(printf "{\"data\": {\"enable-api-fields\": \"%s\", \"enable-git-resolver\": \"%s\", \"enable-hub-resolver\": \"%s\"}}" $1 "${resolver}" "${resolver}") + jsonpatch=$(printf "{\"data\": {\"enable-api-fields\": \"%s\", \"enable-git-resolver\": \"%s\", \"enable-hub-resolver\": \"%s\"}, \"enable-cluster-resolver\": \"%s\"}" $1 "${resolver}" "${resolver}" "${resolver}") echo "feature-flags ConfigMap patch: ${jsonpatch}" kubectl patch configmap feature-flags -n tekton-pipelines -p "$jsonpatch" } diff --git a/test/resolvers_test.go b/test/resolvers_test.go index 66085223a3c..241e3d4425c 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -26,6 +26,7 @@ import ( "testing" "github.com/tektoncd/pipeline/pkg/pod" + "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun" "github.com/tektoncd/pipeline/test/parse" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" knativetest "knative.dev/pkg/test" @@ -42,6 +43,11 @@ var gitFeatureFlags = requireAllGates(map[string]string{ "enable-api-fields": "alpha", }) +var clusterFeatureFlags = requireAllGates(map[string]string{ + "enable-cluster-resolver": "true", + "enable-api-fields": "alpha", +}) + func TestHubResolver(t *testing.T) { ctx := context.Background() c, namespace := setup(ctx, t, hubFeatureFlags) @@ -271,3 +277,108 @@ spec: }) } } + +func TestClusterResolver(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t, clusterFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + pipelineName := helpers.ObjectNameForTest(t) + examplePipeline := parse.MustParsePipeline(t, fmt.Sprintf(` +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: %s + namespace: %s +spec: + tasks: + - name: some-pipeline-task + taskSpec: + steps: + - name: echo + image: ubuntu + script: | + #!/usr/bin/env bash + # Sleep for 10s + sleep 10 +`, pipelineName, namespace)) + + _, err := c.PipelineClient.Create(ctx, examplePipeline, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create Pipeline `%s`: %s", pipelineName, err) + } + + prName := helpers.ObjectNameForTest(t) + + pipelineRun := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + pipelineRef: + resolver: cluster + params: + - name: kind + value: pipeline + - name: name + value: %s + - name: namespace + value: %s +`, prName, namespace, pipelineName, namespace)) + + _, err = c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, PipelineRunSucceed(prName), "PipelineRunSuccess"); err != nil { + t.Fatalf("Error waiting for PipelineRun %s to finish: %s", prName, err) + } +} + +func TestClusterResolver_Failure(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t, clusterFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + prName := helpers.ObjectNameForTest(t) + + pipelineRun := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + pipelineRef: + resolver: cluster + params: + - name: kind + value: pipeline + - name: name + value: does-not-exist + - name: namespace + value: %s +`, prName, namespace, namespace)) + + _, err := c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, + Chain( + FailedWithReason(pipelinerun.ReasonCouldntGetPipeline, prName), + FailedWithMessage("pipelines.tekton.dev \"does-not-exist\" not found", prName), + ), "PipelineRunFailed"); err != nil { + t.Fatalf("Error waiting for PipelineRun to finish with expected error: %s", err) + } +}