diff --git a/cmd/resolvers/main.go b/cmd/resolvers/main.go index f66e9cd0a89..0b4eb1aa89f 100644 --- a/cmd/resolvers/main.go +++ b/cmd/resolvers/main.go @@ -21,12 +21,13 @@ import ( "strings" "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/http" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/hub" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/bundle" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/cluster" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/git" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/http" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/hub" + hubresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/hub" filteredinformerfactory "knative.dev/pkg/client/injection/kube/informers/factory/filtered" "knative.dev/pkg/injection/sharedmain" "knative.dev/pkg/signals" @@ -35,7 +36,7 @@ import ( func main() { ctx := filteredinformerfactory.WithSelectors(signals.NewContext(), v1alpha1.ManagedByLabelKey) tektonHubURL := buildHubURL(os.Getenv("TEKTON_HUB_API"), "") - artifactHubURL := buildHubURL(os.Getenv("ARTIFACT_HUB_API"), hub.DefaultArtifactHubURL) + artifactHubURL := buildHubURL(os.Getenv("ARTIFACT_HUB_API"), hubresolution.DefaultArtifactHubURL) sharedmain.MainWithContext(ctx, "controller", framework.NewController(ctx, &git.Resolver{}), diff --git a/docs/how-to-write-a-resolver.md b/docs/how-to-write-a-resolver.md index 0237fa11daf..94550b1bb12 100644 --- a/docs/how-to-write-a-resolver.md +++ b/docs/how-to-write-a-resolver.md @@ -97,6 +97,29 @@ a little bit of boilerplate. Create `cmd/demoresolver/main.go` with the following setup code: +{{% tabs %}} + +{{% tab "Upgraded Framework" %}} +```go +package main + +import ( + "context" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "knative.dev/pkg/injection/sharedmain" +) + +func main() { + sharedmain.Main("controller", + framework.NewController(context.Background(), &resolver{}), + ) +} + +type resolver struct {} +``` +{{% /tab %}} + +{{% tab "Previous Framework" %}} ```go package main @@ -115,6 +138,10 @@ func main() { type resolver struct {} ``` +{{% /tab %}} + +{{% /tabs %}} + This won't compile yet but you can download the dependencies by running: ```bash @@ -189,6 +216,24 @@ example resolver. We'll also need to add another import for this package at the top: +{{% tabs %}} + +{{% tab "Upgraded Framework" %}} +```go +import ( + "context" + + // Add this one; it defines LabelKeyResolverType we use in GetSelector + "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "knative.dev/pkg/injection/sharedmain" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" +) +``` +{{% /tab %}} + +{{% tab "Previous Framework" %}} + ```go import ( "context" @@ -201,21 +246,48 @@ import ( ) ``` -## The `ValidateParams` method +{{% /tab %}} + +{{% /tabs %}} + +## The `Validate` method -The `ValidateParams` method checks that the params submitted as part of +The `Validate` method checks that the resolution-spec submitted as part of a resolution request are valid. Our example resolver doesn't expect -any params so we'll simply ensure that the given map is empty. +any params in the spec so we'll simply ensure that the there are no params. +In the previous version, this was instead called `ValidateParams` method. See below +for the differences. + +{{% tabs %}} + +{{% tab "Upgraded Framework" %}} ```go -// ValidateParams ensures parameters from a request are as expected. -func (r *resolver) ValidateParams(ctx context.Context, params map[string]string) error { - if len(params) > 0 { +// Validate ensures that the resolution spec from a request is as expected. +func (r *resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + if len(req.Params) > 0 { return errors.New("no params allowed") } return nil } ``` +{{% /tab %}} + +{{% tab "Previous Framework" %}} + +```go +// ValidateParams ensures that the params from a request are as expected. +func (r *resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { + if len(req.Params) > 0 { + return errors.New("no params allowed") + } + return nil +} +``` + +{{% /tab %}} + +{{% /tabs %}} You'll also need to add the `"errors"` package to your list of imports at the top of the file. @@ -228,13 +300,113 @@ going to return a hard-coded string of YAML. Since Tekton Pipelines currently only supports fetching Pipeline resources via remote resolution that's what we'll return. + The method signature we're implementing here has a `framework.ResolvedResource` interface as one of its return values. This is another type we have to implement but it has a small footprint: +{{% tabs %}} + +{{% tab "Upgraded Framework" %}} + + +```go +// Resolve uses the given resolution spec to resolve the requested file or resource. +func (r *resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) { + return &myResolvedResource{}, nil +} + +// our hard-coded resolved file to return +const pipeline = ` +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: my-pipeline +spec: + tasks: + - name: hello-world + taskSpec: + steps: + - image: alpine:3.15.1 + script: | + echo "hello world" +` + +// myResolvedResource wraps the data we want to return to Pipelines +type myResolvedResource struct {} + +// Data returns the bytes of our hard-coded Pipeline +func (*myResolvedResource) Data() []byte { + return []byte(pipeline) +} + +// Annotations returns any metadata needed alongside the data. None atm. +func (*myResolvedResource) Annotations() map[string]string { + return nil +} + +// RefSource is the source reference of the remote data that records where the remote +// file came from including the url, digest and the entrypoint. None atm. +func (*myResolvedResource) RefSource() *pipelinev1.RefSource { + return nil +} +``` + +{{% /tab %}} + +{{% tab "Previous Framework" %}} + + +```go +// Resolve uses the given resolution spec to resolve the requested file or resource. +func (r *resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (framework.ResolvedResource, error) { + return &myResolvedResource{}, nil +} + +// our hard-coded resolved file to return +const pipeline = ` +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: my-pipeline +spec: + tasks: + - name: hello-world + taskSpec: + steps: + - image: alpine:3.15.1 + script: | + echo "hello world" +` + +// myResolvedResource wraps the data we want to return to Pipelines +type myResolvedResource struct {} + +// Data returns the bytes of our hard-coded Pipeline +func (*myResolvedResource) Data() []byte { + return []byte(pipeline) +} + +// Annotations returns any metadata needed alongside the data. None atm. +func (*myResolvedResource) Annotations() map[string]string { + return nil +} + +// RefSource is the source reference of the remote data that records where the remote +// file came from including the url, digest and the entrypoint. None atm. +func (*myResolvedResource) RefSource() *pipelinev1.RefSource { + return nil +} +``` + + +{{% /tab %}} + +{{% /tabs %}} + ```go -// Resolve uses the given params to resolve the requested file or resource. -func (r *resolver) Resolve(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) { +// Resolve uses the given resolution spec to resolve the requested file or resource. +func (r *resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) { return &myResolvedResource{}, nil } diff --git a/docs/resolver-reference.md b/docs/resolver-reference.md index 68fa6fc9273..f2d62e22cf0 100644 --- a/docs/resolver-reference.md +++ b/docs/resolver-reference.md @@ -21,13 +21,33 @@ a resolver](./how-to-write-a-resolver.md). Implementing this interface is required. It provides just enough configuration for the framework to get a resolver running. +{{% tabs %}} + +{{% tab "Upgraded Framework" %}} + +| Method to Implement | Description | +|----------------------|-------------| +| Initialize | Use this method to perform any setup required before the resolver starts receiving requests. | +| GetName | Use this method to return a name to refer to your Resolver by. e.g. `"Git"` | +| GetSelector | Use this method to specify the labels that a resolution request must have to be routed to your resolver. | +| Validate | Use this method to validate the resolution Spec given to your resolver. | +| Resolve | Use this method to perform get the resource based on the ResolutionRequestSpec as input and return it, along with any metadata about it in annotations | + +{{% /tab %}} + +{{% tab "Previous Framework" %}} + | Method to Implement | Description | |----------------------|-------------| | Initialize | Use this method to perform any setup required before the resolver starts receiving requests. | | GetName | Use this method to return a name to refer to your Resolver by. e.g. `"Git"` | | GetSelector | Use this method to specify the labels that a resolution request must have to be routed to your resolver. | -| ValidateParams | Use this method to validate the parameters given to your resolver. | -| Resolve | Use this method to perform get the resource and return it, along with any metadata about it in annotations | +| ValidateParams | Use this method to validate the params given to your resolver. | +| Resolve | Use this method to perform get the resource based on params as input and return it, along with any metadata about it in annotations | + +{{% /tab %}} + +{{% /tabs %}} ## The `ConfigWatcher` Interface @@ -38,7 +58,7 @@ api endpoints or base urls, service account names to use, etc... | Method to Implement | Description | |---------------------|-------------| -| GetConfigName | Use this method to return the name of the configmap admins will use to configure this resolver. Once this interface is implemented your `ValidateParams` and `Resolve` methods will be able to access your latest resolver configuration by calling `framework.GetResolverConfigFromContext(ctx)`. Note that this configmap must exist when your resolver starts - put a default one in your resolver's `config/` directory. | +| GetConfigName | Use this method to return the name of the configmap admins will use to configure this resolver. Once this interface is implemented your `Validate` and `Resolve` methods will be able to access your latest resolver configuration by calling `framework.GetResolverConfigFromContext(ctx)`. Note that this configmap must exist when your resolver starts - put a default one in your resolver's `config/` directory. | ## The `TimedResolution` Interface diff --git a/docs/resolver-template/cmd/resolver/main.go b/docs/resolver-template/cmd/resolver/main.go new file mode 100644 index 00000000000..8bbb01f958c --- /dev/null +++ b/docs/resolver-template/cmd/resolver/main.go @@ -0,0 +1,101 @@ +/* +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 main + +import ( + "context" + "errors" + + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/common" + frameworkV1 "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + filteredinformerfactory "knative.dev/pkg/client/injection/kube/informers/factory/filtered" + "knative.dev/pkg/injection/sharedmain" +) + +func main() { + ctx := filteredinformerfactory.WithSelectors(context.Background(), v1beta1.ManagedByLabelKey) + sharedmain.MainWithContext(ctx, "controller", + framework.NewController(ctx, &resolver{}), + ) +} + +type resolver struct{} + +// Initialize sets up any dependencies needed by the resolver. None atm. +func (r *resolver) Initialize(context.Context) error { + return nil +} + +// GetName returns a string name to refer to this resolver by. +func (r *resolver) GetName(context.Context) string { + return "Demo" +} + +// GetSelector returns a map of labels to match requests to this resolver. +func (r *resolver) GetSelector(context.Context) map[string]string { + return map[string]string{ + common.LabelKeyResolverType: "demo", + } +} + +// Validate ensures resolution spec from a request is as expected. +func (r *resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + if len(req.Params) > 0 { + return errors.New("no params allowed") + } + return nil +} + +// Resolve uses the given resolution spec to resolve the requested file or resource. +func (r *resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (frameworkV1.ResolvedResource, error) { + return &myResolvedResource{}, nil +} + +// our hard-coded resolved file to return +const pipeline = ` +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: my-pipeline +spec: + tasks: + - name: hello-world + taskSpec: + steps: + - image: alpine:3.15.1 + script: | + echo "hello world" +` + +// myResolvedResource wraps the data we want to return to Pipelines +type myResolvedResource struct{} + +// Data returns the bytes of our hard-coded Pipeline +func (*myResolvedResource) Data() []byte { + return []byte(pipeline) +} + +// Annotations returns any metadata needed alongside the data. None atm. +func (*myResolvedResource) Annotations() map[string]string { + return nil +} + +// RefSource is the source reference of the remote data that records where the remote +// file came from including the url, digest and the entrypoint. None atm. +func (*myResolvedResource) RefSource() *pipelinev1.RefSource { + return nil +} diff --git a/docs/resolver-template/cmd/resolver/main_test.go b/docs/resolver-template/cmd/resolver/main_test.go new file mode 100644 index 00000000000..23b9dcf610b --- /dev/null +++ b/docs/resolver-template/cmd/resolver/main_test.go @@ -0,0 +1,67 @@ +/* + 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 main + +import ( + "encoding/base64" + "testing" + "time" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + frtesting "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework/testing" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/test" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "knative.dev/pkg/system/testing" +) + +func TestResolver(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + + r := &resolver{} + + request := &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: "demo", + }, + }, + Spec: v1beta1.ResolutionRequestSpec{}, + } + d := test.Data{ + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + } + + expectedStatus := &v1beta1.ResolutionRequestStatus{ + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString([]byte(pipeline)), + }, + } + + // If you want to test scenarios where an error should occur, pass a non-nil error to RunResolverReconcileTest + var expectedErr error + + frtesting.RunResolverReconcileTest(ctx, t, d, r, request, expectedStatus, expectedErr) +} diff --git a/pkg/resolution/resolver/internal/resolutionrequest.go b/pkg/internal/resolution/resolutionrequest.go similarity index 91% rename from pkg/resolution/resolver/internal/resolutionrequest.go rename to pkg/internal/resolution/resolutionrequest.go index be7f78f9a89..a7cf0909ae0 100644 --- a/pkg/resolution/resolver/internal/resolutionrequest.go +++ b/pkg/internal/resolution/resolutionrequest.go @@ -14,13 +14,13 @@ limitations under the License. */ -package internal +package resolution import ( "encoding/base64" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" corev1 "k8s.io/api/core/v1" "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" @@ -43,7 +43,7 @@ func CreateResolutionRequestFailureStatus() *v1beta1.ResolutionRequestStatus { Conditions: duckv1.Conditions{{ Type: apis.ConditionSucceeded, Status: corev1.ConditionFalse, - Reason: resolutioncommon.ReasonResolutionFailed, + Reason: common.ReasonResolutionFailed, }}, }, } diff --git a/pkg/reconciler/pipelinerun/controller.go b/pkg/reconciler/pipelinerun/controller.go index 5df3f698548..728cd752882 100644 --- a/pkg/reconciler/pipelinerun/controller.go +++ b/pkg/reconciler/pipelinerun/controller.go @@ -33,7 +33,7 @@ import ( "github.com/tektoncd/pipeline/pkg/pipelinerunmetrics" cloudeventclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" - resolution "github.com/tektoncd/pipeline/pkg/resolution/resource" + resolution "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/tracing" "k8s.io/client-go/tools/cache" "k8s.io/utils/clock" diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 8756c1282f4..943e56ae170 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -52,7 +52,7 @@ import ( tresources "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" "github.com/tektoncd/pipeline/pkg/remote" - resolution "github.com/tektoncd/pipeline/pkg/resolution/resource" + resolution "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/substitution" "github.com/tektoncd/pipeline/pkg/trustedresources" "github.com/tektoncd/pipeline/pkg/workspace" diff --git a/pkg/reconciler/pipelinerun/resources/pipelineref.go b/pkg/reconciler/pipelinerun/resources/pipelineref.go index a6674f15483..ff9a1626d76 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelineref.go +++ b/pkg/reconciler/pipelinerun/resources/pipelineref.go @@ -24,12 +24,13 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + resolutionV1beta1 "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" "github.com/tektoncd/pipeline/pkg/reconciler/apiserver" rprp "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun/pipelinespec" "github.com/tektoncd/pipeline/pkg/remote" - "github.com/tektoncd/pipeline/pkg/remote/resolution" - remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource" + "github.com/tektoncd/pipeline/pkg/remoteresolution/remote/resolution" + remoteresource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/trustedresources" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -70,7 +71,12 @@ func GetPipelineFunc(ctx context.Context, k8s kubernetes.Interface, tekton clien } replacedParams := pr.Params.ReplaceVariables(stringReplacements, arrayReplacements, objectReplacements) - resolver := resolution.NewResolver(requester, pipelineRun, string(pr.Resolver), "", "", replacedParams) + resolverPayload := remoteresource.ResolverPayload{ + ResolutionSpec: &resolutionV1beta1.ResolutionRequestSpec{ + Params: replacedParams, + }, + } + resolver := resolution.NewResolver(requester, pipelineRun, string(pr.Resolver), resolverPayload) return resolvePipeline(ctx, resolver, name, namespace, k8s, tekton, verificationPolicies) } default: diff --git a/pkg/reconciler/pipelinerun/resources/pipelineref_test.go b/pkg/reconciler/pipelinerun/resources/pipelineref_test.go index 93d01a52ecc..c20337f4ff1 100644 --- a/pkg/reconciler/pipelinerun/resources/pipelineref_test.go +++ b/pkg/reconciler/pipelinerun/resources/pipelineref_test.go @@ -34,15 +34,18 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + resolutionV1beta1 "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" "github.com/tektoncd/pipeline/pkg/reconciler/apiserver" "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun/resources" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/trustedresources" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" "github.com/tektoncd/pipeline/test/parse" + resolution "github.com/tektoncd/pipeline/test/remoteresolution" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -344,8 +347,8 @@ func TestGetPipelineFunc_RemoteResolution(t *testing.T) { }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resolved := test.NewResolvedResource([]byte(tc.pipelineYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) - requester := test.NewRequester(resolved, nil) + resolved := resolution.NewResolvedResource([]byte(tc.pipelineYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) fn := resources.GetPipelineFunc(ctx, nil, clients, requester, &v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.PipelineRunSpec{ @@ -399,8 +402,8 @@ func TestGetPipelineFunc_RemoteResolution_ValidationFailure(t *testing.T) { }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resolved := test.NewResolvedResource([]byte(tc.pipelineYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) - requester := test.NewRequester(resolved, nil) + resolved := resolution.NewResolvedResource([]byte(tc.pipelineYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) fn := resources.GetPipelineFunc(ctx, nil, clients, requester, &v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.PipelineRunSpec{ @@ -452,16 +455,20 @@ func TestGetPipelineFunc_RemoteResolution_ReplacedParams(t *testing.T) { pipelineYAMLString, }, "\n") - resolved := test.NewResolvedResource([]byte(pipelineYAML), nil, sampleRefSource.DeepCopy(), nil) - requester := &test.Requester{ + resolved := resolution.NewResolvedResource([]byte(pipelineYAML), nil, sampleRefSource.DeepCopy(), nil) + requester := &resolution.Requester{ ResolvedResource: resolved, - Params: v1.Params{{ - Name: "foo", - Value: *v1.NewStructuredValues("bar"), - }, { - Name: "bar", - Value: *v1.NewStructuredValues("test-pipeline"), - }}, + ResolverPayload: resource.ResolverPayload{ + ResolutionSpec: &resolutionV1beta1.ResolutionRequestSpec{ + Params: v1.Params{{ + Name: "foo", + Value: *v1.NewStructuredValues("bar"), + }, { + Name: "bar", + Value: *v1.NewStructuredValues("test-pipeline"), + }}, + }, + }, } fn := resources.GetPipelineFunc(ctx, nil, clients, requester, &v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{ @@ -538,8 +545,8 @@ func TestGetPipelineFunc_RemoteResolutionInvalidData(t *testing.T) { ctx = config.ToContext(ctx, cfg) pipelineRef := &v1.PipelineRef{ResolverRef: v1.ResolverRef{Resolver: "git"}} resolvesTo := []byte("INVALID YAML") - resource := test.NewResolvedResource(resolvesTo, nil, nil, nil) - requester := test.NewRequester(resource, nil) + res := resolution.NewResolvedResource(resolvesTo, nil, nil, nil) + requester := resolution.NewRequester(res, nil, resource.ResolverPayload{}) fn := resources.GetPipelineFunc(ctx, nil, clients, requester, &v1.PipelineRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.PipelineRunSpec{ @@ -577,8 +584,8 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyNoError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedUnmatched := test.NewResolvedResource(unsignedPipelineBytes, nil, noMatchPolicyRefSource, nil) - requesterUnmatched := test.NewRequester(resolvedUnmatched, nil) + resolvedUnmatched := resolution.NewResolvedResource(unsignedPipelineBytes, nil, noMatchPolicyRefSource, nil) + requesterUnmatched := resolution.NewRequester(resolvedUnmatched, nil, resource.ResolverPayload{}) signedPipeline, err := test.GetSignedV1beta1Pipeline(unsignedPipeline, signer, "signed") if err != nil { @@ -600,8 +607,8 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyNoError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedMatched := test.NewResolvedResource(signedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterMatched := test.NewRequester(resolvedMatched, nil) + resolvedMatched := resolution.NewResolvedResource(signedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterMatched := resolution.NewRequester(resolvedMatched, nil, resource.ResolverPayload{}) pipelineRef := &v1.PipelineRef{ Name: signedPipeline.Name, @@ -647,12 +654,12 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyNoError(t *testing.T) { warnPolicyRefSource := &v1.RefSource{ URI: " warnVP", } - resolvedUnsignedMatched := test.NewResolvedResource(unsignedPipelineBytes, nil, warnPolicyRefSource, nil) - requesterUnsignedMatched := test.NewRequester(resolvedUnsignedMatched, nil) + resolvedUnsignedMatched := resolution.NewResolvedResource(unsignedPipelineBytes, nil, warnPolicyRefSource, nil) + requesterUnsignedMatched := resolution.NewRequester(resolvedUnsignedMatched, nil, resource.ResolverPayload{}) testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string pipelinerun v1.PipelineRun policies []*v1alpha1.VerificationPolicy @@ -778,8 +785,8 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyError(t *testing.T) { EntryPoint: "foo/bar", } - resolvedUnsigned := test.NewResolvedResource(unsignedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterUnsigned := test.NewRequester(resolvedUnsigned, nil) + resolvedUnsigned := resolution.NewResolvedResource(unsignedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterUnsigned := resolution.NewRequester(resolvedUnsigned, nil, resource.ResolverPayload{}) signedPipeline, err := test.GetSignedV1beta1Pipeline(unsignedPipeline, signer, "signed") if err != nil { @@ -797,8 +804,8 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedUnmatched := test.NewResolvedResource(signedPipelineBytes, nil, noMatchPolicyRefSource, nil) - requesterUnmatched := test.NewRequester(resolvedUnmatched, nil) + resolvedUnmatched := resolution.NewResolvedResource(signedPipelineBytes, nil, noMatchPolicyRefSource, nil) + requesterUnmatched := resolution.NewRequester(resolvedUnmatched, nil, resource.ResolverPayload{}) modifiedPipeline := signedPipeline.DeepCopy() modifiedPipeline.Annotations["random"] = "attack" @@ -806,14 +813,14 @@ func TestGetPipelineFunc_V1beta1Pipeline_VerifyError(t *testing.T) { if err != nil { t.Fatal("fail to marshal pipeline", err) } - resolvedModified := test.NewResolvedResource(modifiedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterModified := test.NewRequester(resolvedModified, nil) + resolvedModified := resolution.NewResolvedResource(modifiedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterModified := resolution.NewRequester(resolvedModified, nil, resource.ResolverPayload{}) pipelineRef := &v1.PipelineRef{ResolverRef: v1.ResolverRef{Resolver: "git"}} testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string expectedVerificationResult *trustedresources.VerificationResult }{ @@ -906,8 +913,8 @@ func TestGetPipelineFunc_V1Pipeline_VerifyNoError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedUnmatched := test.NewResolvedResource(unsignedPipelineBytes, nil, noMatchPolicyRefSource, nil) - requesterUnmatched := test.NewRequester(resolvedUnmatched, nil) + resolvedUnmatched := resolution.NewResolvedResource(unsignedPipelineBytes, nil, noMatchPolicyRefSource, nil) + requesterUnmatched := resolution.NewRequester(resolvedUnmatched, nil, resource.ResolverPayload{}) signedPipeline, err := getSignedV1Pipeline(unsignedV1Pipeline, signer, "signed") if err != nil { @@ -935,8 +942,8 @@ func TestGetPipelineFunc_V1Pipeline_VerifyNoError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedMatched := test.NewResolvedResource(signedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterMatched := test.NewRequester(resolvedMatched, nil) + resolvedMatched := resolution.NewResolvedResource(signedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterMatched := resolution.NewRequester(resolvedMatched, nil, resource.ResolverPayload{}) pipelineRef := &v1.PipelineRef{ Name: signedPipeline.Name, @@ -980,12 +987,12 @@ func TestGetPipelineFunc_V1Pipeline_VerifyNoError(t *testing.T) { warnPolicyRefSource := &v1.RefSource{ URI: " warnVP", } - resolvedUnsignedMatched := test.NewResolvedResource(unsignedPipelineBytes, nil, warnPolicyRefSource, nil) - requesterUnsignedMatched := test.NewRequester(resolvedUnsignedMatched, nil) + resolvedUnsignedMatched := resolution.NewResolvedResource(unsignedPipelineBytes, nil, warnPolicyRefSource, nil) + requesterUnsignedMatched := resolution.NewRequester(resolvedUnsignedMatched, nil, resource.ResolverPayload{}) testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string pipelinerun v1.PipelineRun policies []*v1alpha1.VerificationPolicy @@ -1110,8 +1117,8 @@ func TestGetPipelineFunc_V1Pipeline_VerifyError(t *testing.T) { EntryPoint: "foo/bar", } - resolvedUnsigned := test.NewResolvedResource(unsignedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterUnsigned := test.NewRequester(resolvedUnsigned, nil) + resolvedUnsigned := resolution.NewResolvedResource(unsignedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterUnsigned := resolution.NewRequester(resolvedUnsigned, nil, resource.ResolverPayload{}) signedPipeline, err := getSignedV1Pipeline(unsignedV1Pipeline, signer, "signed") if err != nil { @@ -1129,8 +1136,8 @@ func TestGetPipelineFunc_V1Pipeline_VerifyError(t *testing.T) { }, EntryPoint: "foo/bar", } - resolvedUnmatched := test.NewResolvedResource(signedPipelineBytes, nil, noMatchPolicyRefSource, nil) - requesterUnmatched := test.NewRequester(resolvedUnmatched, nil) + resolvedUnmatched := resolution.NewResolvedResource(signedPipelineBytes, nil, noMatchPolicyRefSource, nil) + requesterUnmatched := resolution.NewRequester(resolvedUnmatched, nil, resource.ResolverPayload{}) modifiedPipeline := signedPipeline.DeepCopy() modifiedPipeline.Annotations["random"] = "attack" @@ -1138,14 +1145,14 @@ func TestGetPipelineFunc_V1Pipeline_VerifyError(t *testing.T) { if err != nil { t.Fatal("fail to marshal pipeline", err) } - resolvedModified := test.NewResolvedResource(modifiedPipelineBytes, nil, matchPolicyRefSource, nil) - requesterModified := test.NewRequester(resolvedModified, nil) + resolvedModified := resolution.NewResolvedResource(modifiedPipelineBytes, nil, matchPolicyRefSource, nil) + requesterModified := resolution.NewRequester(resolvedModified, nil, resource.ResolverPayload{}) pipelineRef := &v1.PipelineRef{ResolverRef: v1.ResolverRef{Resolver: "git"}} testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string expectedVerificationResult *trustedresources.VerificationResult }{ @@ -1221,8 +1228,8 @@ func TestGetPipelineFunc_GetFuncError(t *testing.T) { t.Fatal("fail to marshal pipeline", err) } - resolvedUnsigned := test.NewResolvedResource(unsignedPipelineBytes, nil, sampleRefSource.DeepCopy(), nil) - requesterUnsigned := test.NewRequester(resolvedUnsigned, nil) + resolvedUnsigned := resolution.NewResolvedResource(unsignedPipelineBytes, nil, sampleRefSource.DeepCopy(), nil) + requesterUnsigned := resolution.NewRequester(resolvedUnsigned, nil, resource.ResolverPayload{}) resolvedUnsigned.DataErr = errors.New("resolution error") prResolutionError := &v1.PipelineRun{ @@ -1242,7 +1249,7 @@ func TestGetPipelineFunc_GetFuncError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester pipelinerun v1.PipelineRun expectedErr error }{ diff --git a/pkg/reconciler/taskrun/controller.go b/pkg/reconciler/taskrun/controller.go index 451c78f9795..024abd29373 100644 --- a/pkg/reconciler/taskrun/controller.go +++ b/pkg/reconciler/taskrun/controller.go @@ -31,7 +31,7 @@ import ( "github.com/tektoncd/pipeline/pkg/pod" cloudeventclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" - resolution "github.com/tektoncd/pipeline/pkg/resolution/resource" + resolution "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/taskrunmetrics" "github.com/tektoncd/pipeline/pkg/tracing" diff --git a/pkg/reconciler/taskrun/resources/taskref.go b/pkg/reconciler/taskrun/resources/taskref.go index 368ce8b78c7..5d5346c4791 100644 --- a/pkg/reconciler/taskrun/resources/taskref.go +++ b/pkg/reconciler/taskrun/resources/taskref.go @@ -25,11 +25,12 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + resolutionV1beta1 "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" "github.com/tektoncd/pipeline/pkg/reconciler/apiserver" "github.com/tektoncd/pipeline/pkg/remote" - "github.com/tektoncd/pipeline/pkg/remote/resolution" - remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource" + "github.com/tektoncd/pipeline/pkg/remoteresolution/remote/resolution" + remoteresource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/trustedresources" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -108,7 +109,14 @@ func GetTaskFunc(ctx context.Context, k8s kubernetes.Interface, tekton clientset } else { replacedParams = append(replacedParams, tr.Params...) } - resolver := resolution.NewResolver(requester, owner, string(tr.Resolver), trName, namespace, replacedParams) + resolverPayload := remoteresource.ResolverPayload{ + Name: trName, + Namespace: namespace, + ResolutionSpec: &resolutionV1beta1.ResolutionRequestSpec{ + Params: replacedParams, + }, + } + resolver := resolution.NewResolver(requester, owner, string(tr.Resolver), resolverPayload) return resolveTask(ctx, resolver, name, namespace, kind, k8s, tekton, verificationPolicies) } @@ -136,7 +144,14 @@ func GetStepActionFunc(tekton clientset.Interface, k8s kubernetes.Interface, req return func(ctx context.Context, name string) (*v1alpha1.StepAction, *v1.RefSource, error) { // Perform params replacements for StepAction resolver params ApplyParameterSubstitutionInResolverParams(tr, step) - resolver := resolution.NewResolver(requester, tr, string(step.Ref.Resolver), trName, namespace, step.Ref.Params) + resolverPayload := remoteresource.ResolverPayload{ + Name: trName, + Namespace: namespace, + ResolutionSpec: &resolutionV1beta1.ResolutionRequestSpec{ + Params: step.Ref.Params, + }, + } + resolver := resolution.NewResolver(requester, tr, string(step.Ref.Resolver), resolverPayload) return resolveStepAction(ctx, resolver, name, namespace, k8s, tekton) } } diff --git a/pkg/reconciler/taskrun/resources/taskref_test.go b/pkg/reconciler/taskrun/resources/taskref_test.go index e2d6ca79e24..ae05833873c 100644 --- a/pkg/reconciler/taskrun/resources/taskref_test.go +++ b/pkg/reconciler/taskrun/resources/taskref_test.go @@ -34,13 +34,16 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + resolutionV1beta1 "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" "github.com/tektoncd/pipeline/pkg/reconciler/apiserver" "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/trustedresources" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" "github.com/tektoncd/pipeline/test/parse" + resolution "github.com/tektoncd/pipeline/test/remoteresolution" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -897,8 +900,8 @@ func TestGetStepActionFunc_RemoteResolution_Success(t *testing.T) { }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resolved := test.NewResolvedResource([]byte(tc.stepActionYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) - requester := test.NewRequester(resolved, nil) + resolved := resolution.NewResolvedResource([]byte(tc.stepActionYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.TaskRunSpec{ @@ -958,8 +961,8 @@ func TestGetStepActionFunc_RemoteResolution_Error(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resource := test.NewResolvedResource(tc.resolvesTo, nil, nil, nil) - requester := test.NewRequester(resource, nil) + res := resolution.NewResolvedResource(tc.resolvesTo, nil, nil, nil) + requester := resolution.NewRequester(res, nil, resource.ResolverPayload{}) tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.TaskRunSpec{ @@ -1090,8 +1093,8 @@ func TestGetTaskFunc_RemoteResolution(t *testing.T) { }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resolved := test.NewResolvedResource([]byte(tc.taskYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) - requester := test.NewRequester(resolved, nil) + resolved := resolution.NewResolvedResource([]byte(tc.taskYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.TaskRunSpec{ @@ -1157,8 +1160,8 @@ func TestGetTaskFunc_RemoteResolution_ValidationFailure(t *testing.T) { }} for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - resolved := test.NewResolvedResource([]byte(tc.taskYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) - requester := test.NewRequester(resolved, nil) + resolved := resolution.NewResolvedResource([]byte(tc.taskYAML), nil /* annotations */, sampleRefSource.DeepCopy(), nil /* data error */) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) tektonclient := fake.NewSimpleClientset() fn := resources.GetTaskFunc(ctx, nil, tektonclient, requester, &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, @@ -1208,16 +1211,20 @@ func TestGetTaskFunc_RemoteResolution_ReplacedParams(t *testing.T) { taskYAMLString, }, "\n") - resolved := test.NewResolvedResource([]byte(taskYAML), nil, sampleRefSource.DeepCopy(), nil) - requester := &test.Requester{ + resolved := resolution.NewResolvedResource([]byte(taskYAML), nil, sampleRefSource.DeepCopy(), nil) + requester := &resolution.Requester{ ResolvedResource: resolved, - Params: v1.Params{{ - Name: "foo", - Value: *v1.NewStructuredValues("bar"), - }, { - Name: "bar", - Value: *v1.NewStructuredValues("test-task"), - }}, + ResolverPayload: resource.ResolverPayload{ + ResolutionSpec: &resolutionV1beta1.ResolutionRequestSpec{ + Params: v1.Params{{ + Name: "foo", + Value: *v1.NewStructuredValues("bar"), + }, { + Name: "bar", + Value: *v1.NewStructuredValues("test-task"), + }}, + }, + }, } tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{ @@ -1293,8 +1300,8 @@ func TestGetPipelineFunc_RemoteResolutionInvalidData(t *testing.T) { ctx = config.ToContext(ctx, cfg) taskRef := &v1.TaskRef{ResolverRef: v1.ResolverRef{Resolver: "git"}} resolvesTo := []byte("INVALID YAML") - resource := test.NewResolvedResource(resolvesTo, nil, nil, nil) - requester := test.NewRequester(resource, nil) + res := resolution.NewResolvedResource(resolvesTo, nil, nil, nil) + requester := resolution.NewRequester(res, nil, resource.ResolverPayload{}) tr := &v1.TaskRun{ ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, Spec: v1.TaskRunSpec{ @@ -1355,7 +1362,7 @@ func TestGetTaskFunc_V1beta1Task_VerifyNoError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string policies []*v1alpha1.VerificationPolicy expected runtime.Object @@ -1484,7 +1491,7 @@ func TestGetTaskFunc_V1beta1Task_VerifyError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string expected *v1.Task expectedErr error @@ -1621,7 +1628,7 @@ func TestGetTaskFunc_V1Task_VerifyNoError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string policies []*v1alpha1.VerificationPolicy expected runtime.Object @@ -1750,7 +1757,7 @@ func TestGetTaskFunc_V1Task_VerifyError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester verificationNoMatchPolicy string expected *v1.Task expectedErr error @@ -1837,8 +1844,8 @@ func TestGetTaskFunc_GetFuncError(t *testing.T) { t.Fatal("fail to marshal task", err) } - resolvedUnsigned := test.NewResolvedResource(unsignedTaskBytes, nil, sampleRefSource.DeepCopy(), nil) - requesterUnsigned := test.NewRequester(resolvedUnsigned, nil) + resolvedUnsigned := resolution.NewResolvedResource(unsignedTaskBytes, nil, sampleRefSource.DeepCopy(), nil) + requesterUnsigned := resolution.NewRequester(resolvedUnsigned, nil, resource.ResolverPayload{}) resolvedUnsigned.DataErr = errors.New("resolution error") trResolutionError := &v1.TaskRun{ @@ -1856,7 +1863,7 @@ func TestGetTaskFunc_GetFuncError(t *testing.T) { testcases := []struct { name string - requester *test.Requester + requester *resolution.Requester taskrun v1.TaskRun expectedErr error }{ @@ -1926,9 +1933,9 @@ spec: - name: foo ` -func bytesToRequester(data []byte, source *v1.RefSource) *test.Requester { - resolved := test.NewResolvedResource(data, nil, source, nil) - requester := test.NewRequester(resolved, nil) +func bytesToRequester(data []byte, source *v1.RefSource) *resolution.Requester { + resolved := resolution.NewResolvedResource(data, nil, source, nil) + requester := resolution.NewRequester(resolved, nil, resource.ResolverPayload{}) return requester } diff --git a/pkg/reconciler/taskrun/resources/taskspec.go b/pkg/reconciler/taskrun/resources/taskspec.go index 64d71df04e8..955154911c9 100644 --- a/pkg/reconciler/taskrun/resources/taskspec.go +++ b/pkg/reconciler/taskrun/resources/taskspec.go @@ -25,7 +25,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" resolutionutil "github.com/tektoncd/pipeline/pkg/internal/resolution" - remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource" + remoteresource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/trustedresources" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 4259f1f9279..ebd744d05b7 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -47,7 +47,7 @@ import ( "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" "github.com/tektoncd/pipeline/pkg/remote" - resolution "github.com/tektoncd/pipeline/pkg/resolution/resource" + resolution "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/taskrunmetrics" _ "github.com/tektoncd/pipeline/pkg/taskrunmetrics/fake" // Make sure the taskrunmetrics are setup diff --git a/pkg/remote/resolution/error.go b/pkg/remote/resolution/error.go index 05022c5f8cb..9621060e074 100644 --- a/pkg/remote/resolution/error.go +++ b/pkg/remote/resolution/error.go @@ -36,7 +36,7 @@ var ( // InvalidRuntimeObjectError is returned when remote resolution // succeeded but the returned data is not a valid runtime.Object. type InvalidRuntimeObjectError struct { - original error + Original error } // ErrorInvalidRuntimeObject is an alias to InvalidRuntimeObjectError. @@ -51,12 +51,12 @@ var ( // Error returns the string representation of this error. func (e *InvalidRuntimeObjectError) Error() string { - return fmt.Sprintf("invalid runtime object: %v", e.original) + return fmt.Sprintf("invalid runtime object: %v", e.Original) } // Unwrap returns the underlying original error. func (e *InvalidRuntimeObjectError) Unwrap() error { - return e.original + return e.Original } // Is returns true if the given error coerces into an error of this type. @@ -68,7 +68,7 @@ func (e *InvalidRuntimeObjectError) Is(that error) bool { // attempting to access the resolved data failed. An example of this // type of error would be if a ResolutionRequest contained malformed base64. type DataAccessError struct { - original error + Original error } // ErrorAccessingData is an alias to DataAccessError @@ -83,12 +83,12 @@ var ( // Error returns the string representation of this error. func (e *DataAccessError) Error() string { - return fmt.Sprintf("error accessing data from remote resource: %v", e.original) + return fmt.Sprintf("error accessing data from remote resource: %v", e.Original) } // Unwrap returns the underlying original error. func (e *DataAccessError) Unwrap() error { - return e.original + return e.Original } // Is returns true if the given error coerces into an error of this type. diff --git a/pkg/remote/resolution/resolver.go b/pkg/remote/resolution/resolver.go index 772b39e416a..fb164924f03 100644 --- a/pkg/remote/resolution/resolver.go +++ b/pkg/remote/resolution/resolver.go @@ -63,6 +63,27 @@ func (resolver *Resolver) Get(ctx context.Context, _, _ string) (runtime.Object, return nil, nil, fmt.Errorf("error building request for remote resource: %w", err) } resolved, err := resolver.requester.Submit(ctx, resolverName, req) + return ResolvedRequest(resolved, err) +} + +// List implements remote.Resolver but is unused for remote resolution. +func (resolver *Resolver) List(_ context.Context) ([]remote.ResolvedObject, error) { + return nil, nil +} + +func buildRequest(resolverName string, owner kmeta.OwnerRefable, name string, namespace string, params v1.Params) (*resolutionRequest, error) { + name, namespace, err := remoteresource.GetNameAndNamespace(resolverName, owner, name, namespace, params) + if err != nil { + return nil, err + } + req := &resolutionRequest{ + Request: remoteresource.NewRequest(name, namespace, params), + owner: owner, + } + return req, nil +} + +func ResolvedRequest(resolved resolutioncommon.ResolvedResource, err error) (runtime.Object, *v1.RefSource, error) { switch { case errors.Is(err, resolutioncommon.ErrRequestInProgress): return nil, nil, remote.ErrRequestInProgress @@ -74,39 +95,11 @@ func (resolver *Resolver) Get(ctx context.Context, _, _ string) (runtime.Object, } data, err := resolved.Data() if err != nil { - return nil, nil, &DataAccessError{original: err} + return nil, nil, &DataAccessError{Original: err} } obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(data, nil, nil) if err != nil { - return nil, nil, &InvalidRuntimeObjectError{original: err} + return nil, nil, &InvalidRuntimeObjectError{Original: err} } return obj, resolved.RefSource(), nil } - -// List implements remote.Resolver but is unused for remote resolution. -func (resolver *Resolver) List(_ context.Context) ([]remote.ResolvedObject, error) { - return nil, nil -} - -func buildRequest(resolverName string, owner kmeta.OwnerRefable, name string, namespace string, params v1.Params) (*resolutionRequest, error) { - if name == "" { - name = owner.GetObjectMeta().GetName() - namespace = owner.GetObjectMeta().GetNamespace() - } - if namespace == "" { - namespace = "default" - } - // Generating a deterministic name for the resource request - // prevents multiple requests being issued for the same - // pipelinerun's pipelineRef or taskrun's taskRef. - remoteResourceBaseName := namespace + "/" + name - name, err := remoteresource.GenerateDeterministicName(resolverName, remoteResourceBaseName, params) - if err != nil { - return nil, fmt.Errorf("error generating name for taskrun %s/%s: %w", namespace, name, err) - } - req := &resolutionRequest{ - Request: remoteresource.NewRequest(name, namespace, params), - owner: owner, - } - return req, nil -} diff --git a/pkg/remote/resolution/resolver_test.go b/pkg/remote/resolution/resolver_test.go index 8e900ba50ea..6f078ac2851 100644 --- a/pkg/remote/resolution/resolver_test.go +++ b/pkg/remote/resolution/resolver_test.go @@ -23,8 +23,8 @@ import ( "github.com/tektoncd/pipeline/pkg/remote" resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" remoteresource "github.com/tektoncd/pipeline/pkg/resolution/resource" - "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" + resolution "github.com/tektoncd/pipeline/test/resolution" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/kmeta" ) @@ -60,11 +60,11 @@ func TestGet_Successful(t *testing.T) { Namespace: "bar", }, } - resolved := &test.ResolvedResource{ + resolved := &resolution.ResolvedResource{ ResolvedData: tc.resolvedData, ResolvedAnnotations: tc.resolvedAnnotations, } - requester := &test.Requester{ + requester := &resolution.Requester{ SubmitErr: nil, ResolvedResource: resolved, } @@ -77,11 +77,11 @@ func TestGet_Successful(t *testing.T) { func TestGet_Errors(t *testing.T) { genericError := errors.New("uh oh something bad happened") - notARuntimeObject := &test.ResolvedResource{ + notARuntimeObject := &resolution.ResolvedResource{ ResolvedData: []byte(">:)"), ResolvedAnnotations: nil, } - invalidDataResource := &test.ResolvedResource{ + invalidDataResource := &resolution.ResolvedResource{ DataErr: errors.New("data access error"), ResolvedAnnotations: nil, } @@ -117,7 +117,7 @@ func TestGet_Errors(t *testing.T) { Namespace: "bar", }, } - requester := &test.Requester{ + requester := &resolution.Requester{ SubmitErr: tc.submitErr, ResolvedResource: tc.resolvedResource, } diff --git a/pkg/remoteresolution/doc.go b/pkg/remoteresolution/doc.go new file mode 100644 index 00000000000..a02110db834 --- /dev/null +++ b/pkg/remoteresolution/doc.go @@ -0,0 +1,23 @@ +/* +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 remoteresolution contains the upgraded remote resolution framework. +This was necessary to ensure backwards compatibility with the existing framework. + +This framework is `ALPHA` and subject to further refactoring and changes. +*/ +package remoteresolution diff --git a/pkg/remoteresolution/remote/resolution/doc.go b/pkg/remoteresolution/remote/resolution/doc.go new file mode 100644 index 00000000000..50255308559 --- /dev/null +++ b/pkg/remoteresolution/remote/resolution/doc.go @@ -0,0 +1,24 @@ +/* +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 resolution contains the upgraded remote resolution framework. +It is equivalent to `pkg/remote/resolution`. +This was necessary to ensure backwards compatibility with the existing framework. + +This framework is `ALPHA` and subject to further refactoring and changes. +*/ +package resolution diff --git a/pkg/remoteresolution/remote/resolution/request.go b/pkg/remoteresolution/remote/resolution/request.go new file mode 100644 index 00000000000..5a22f414014 --- /dev/null +++ b/pkg/remoteresolution/remote/resolution/request.go @@ -0,0 +1,32 @@ +/* +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 resolution + +import ( + resolution "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/kmeta" +) + +var _ resolution.Request = &resolutionRequest{} +var _ resolution.OwnedRequest = &resolutionRequest{} + +type resolutionRequest struct { + resolution.Request + owner kmeta.OwnerRefable +} + +func (req *resolutionRequest) OwnerRef() metav1.OwnerReference { + return *kmeta.NewControllerRef(req.owner) +} diff --git a/pkg/remoteresolution/remote/resolution/resolver.go b/pkg/remoteresolution/remote/resolution/resolver.go new file mode 100644 index 00000000000..85836addbeb --- /dev/null +++ b/pkg/remoteresolution/remote/resolution/resolver.go @@ -0,0 +1,91 @@ +/* +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 resolution + +import ( + "context" + "fmt" + + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/remote" + resolution "github.com/tektoncd/pipeline/pkg/remote/resolution" + remoteresource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + resource "github.com/tektoncd/pipeline/pkg/resolution/resource" + "k8s.io/apimachinery/pkg/runtime" + "knative.dev/pkg/kmeta" +) + +// Resolver implements remote.Resolver and encapsulates the majority of +// code required to interface with the tektoncd/resolution project. It +// is used to make async requests for resources like pipelines from +// remote places like git repos. +type Resolver struct { + requester remoteresource.Requester + owner kmeta.OwnerRefable + resolverName string + resolverPayload remoteresource.ResolverPayload +} + +var _ remote.Resolver = &Resolver{} + +// NewResolver returns an implementation of remote.Resolver capable +// of performing asynchronous remote resolution. +func NewResolver(requester remoteresource.Requester, owner kmeta.OwnerRefable, resolverName string, resolverPayload remoteresource.ResolverPayload) remote.Resolver { + return &Resolver{ + requester: requester, + owner: owner, + resolverName: resolverName, + resolverPayload: resolverPayload, + } +} + +// Get implements remote.Resolver. +func (resolver *Resolver) Get(ctx context.Context, _, _ string) (runtime.Object, *v1.RefSource, error) { + resolverName := remoteresource.ResolverName(resolver.resolverName) + req, err := buildRequest(resolver.resolverName, resolver.owner, &resolver.resolverPayload) + if err != nil { + return nil, nil, fmt.Errorf("error building request for remote resource: %w", err) + } + resolved, err := resolver.requester.Submit(ctx, resolverName, req) + return resolution.ResolvedRequest(resolved, err) +} + +// List implements remote.Resolver but is unused for remote resolution. +func (resolver *Resolver) List(_ context.Context) ([]remote.ResolvedObject, error) { + return nil, nil +} + +func buildRequest(resolverName string, owner kmeta.OwnerRefable, resolverPayload *remoteresource.ResolverPayload) (*resolutionRequest, error) { + var name string + var namespace string + var params v1.Params + if resolverPayload != nil { + name = resolverPayload.Name + namespace = resolverPayload.Namespace + if resolverPayload.ResolutionSpec != nil { + params = resolverPayload.ResolutionSpec.Params + } + } + name, namespace, err := resource.GetNameAndNamespace(resolverName, owner, name, namespace, params) + if err != nil { + return nil, err + } + resolverPayload.Name = name + resolverPayload.Namespace = namespace + req := &resolutionRequest{ + Request: remoteresource.NewRequest(*resolverPayload), + owner: owner, + } + return req, nil +} diff --git a/pkg/remoteresolution/remote/resolution/resolver_test.go b/pkg/remoteresolution/remote/resolution/resolver_test.go new file mode 100644 index 00000000000..e93f43ef4f2 --- /dev/null +++ b/pkg/remoteresolution/remote/resolution/resolver_test.go @@ -0,0 +1,181 @@ +/* +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 resolution + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/remote" + "github.com/tektoncd/pipeline/pkg/remote/resolution" + remoteresource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resource" + "github.com/tektoncd/pipeline/test/diff" + test "github.com/tektoncd/pipeline/test/remoteresolution" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/kmeta" +) + +var pipelineBytes = []byte(` +kind: Pipeline +apiVersion: tekton.dev/v1beta1 +metadata: + name: foo +spec: + tasks: + - name: task1 + taskSpec: + steps: + - name: step1 + image: ubuntu + script: | + echo "hello world!" +`) + +func TestGet_Successful(t *testing.T) { + for _, tc := range []struct { + resolvedData []byte + resolvedAnnotations map[string]string + }{{ + resolvedData: pipelineBytes, + resolvedAnnotations: nil, + }} { + ctx := context.Background() + owner := &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + resolved := &test.ResolvedResource{ + ResolvedData: tc.resolvedData, + ResolvedAnnotations: tc.resolvedAnnotations, + } + requester := &test.Requester{ + SubmitErr: nil, + ResolvedResource: resolved, + } + resolver := NewResolver(requester, owner, "git", remoteresource.ResolverPayload{}) + if _, _, err := resolver.Get(ctx, "foo", "bar"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + } +} + +func TestGet_Errors(t *testing.T) { + genericError := errors.New("uh oh something bad happened") + notARuntimeObject := &test.ResolvedResource{ + ResolvedData: []byte(">:)"), + ResolvedAnnotations: nil, + } + invalidDataResource := &test.ResolvedResource{ + DataErr: errors.New("data access error"), + ResolvedAnnotations: nil, + } + for _, tc := range []struct { + submitErr error + expectedGetErr error + resolvedResource remoteresource.ResolvedResource + }{{ + submitErr: common.ErrRequestInProgress, + expectedGetErr: remote.ErrRequestInProgress, + resolvedResource: nil, + }, { + submitErr: nil, + expectedGetErr: resolution.ErrNilResource, + resolvedResource: nil, + }, { + submitErr: genericError, + expectedGetErr: genericError, + resolvedResource: nil, + }, { + submitErr: nil, + expectedGetErr: &resolution.InvalidRuntimeObjectError{}, + resolvedResource: notARuntimeObject, + }, { + submitErr: nil, + expectedGetErr: &resolution.DataAccessError{}, + resolvedResource: invalidDataResource, + }} { + ctx := context.Background() + owner := &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + requester := &test.Requester{ + SubmitErr: tc.submitErr, + ResolvedResource: tc.resolvedResource, + } + resolver := NewResolver(requester, owner, "git", remoteresource.ResolverPayload{}) + obj, refSource, err := resolver.Get(ctx, "foo", "bar") + if obj != nil { + t.Errorf("received unexpected resolved resource") + } + if refSource != nil { + t.Errorf("expected refSource is nil, but received %v", refSource) + } + if !errors.Is(err, tc.expectedGetErr) { + t.Fatalf("expected %v received %v", tc.expectedGetErr, err) + } + } +} + +func TestBuildRequestV2(t *testing.T) { + for _, tc := range []struct { + name string + targetName string + targetNamespace string + }{{ + name: "just owner", + }, { + name: "with target name and namespace", + targetName: "some-object", + targetNamespace: "some-ns", + }} { + t.Run(tc.name, func(t *testing.T) { + owner := &v1beta1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + + rr := &remoteresource.ResolverPayload{Name: tc.targetName, Namespace: tc.targetNamespace} + req, err := buildRequest("git", owner, rr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if d := cmp.Diff(*kmeta.NewControllerRef(owner), req.OwnerRef()); d != "" { + t.Errorf("expected matching owner ref but got %s", diff.PrintWantGot(d)) + } + reqNameBase := owner.Namespace + "/" + owner.Name + if tc.targetName != "" { + reqNameBase = tc.targetNamespace + "/" + tc.targetName + } + expectedReqName, err := resource.GenerateDeterministicNameFromSpec("git", reqNameBase, rr.ResolutionSpec) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if expectedReqName != req.ResolverPayload().Name { + t.Errorf("expected request name %s, but was %s", expectedReqName, req.ResolverPayload().Name) + } + }) + } +} diff --git a/pkg/remoteresolution/resolver/bundle/resolver.go b/pkg/remoteresolution/resolver/bundle/resolver.go new file mode 100644 index 00000000000..85abd28672f --- /dev/null +++ b/pkg/remoteresolution/resolver/bundle/resolver.go @@ -0,0 +1,78 @@ +/* + 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 bundle + +import ( + "context" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "k8s.io/client-go/kubernetes" + "knative.dev/pkg/client/injection/kube/client" +) + +const ( + // LabelValueBundleResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueBundleResolverType string = "bundles" + + // BundleResolverName is the name that the bundle resolver should be associated with. + BundleResolverName = "bundleresolver" +) + +var _ framework.Resolver = &Resolver{} + +// Resolver implements a framework.Resolver that can fetch files from OCI bundles. +type Resolver struct { + kubeClientSet kubernetes.Interface +} + +// Initialize sets up any dependencies needed by the Resolver. None atm. +func (r *Resolver) Initialize(ctx context.Context) error { + r.kubeClientSet = client.Get(ctx) + return nil +} + +// GetName returns a string name to refer to this Resolver by. +func (r *Resolver) GetName(context.Context) string { + return BundleResolverName +} + +// GetConfigName returns the name of the bundle resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return bundle.ConfigMapName +} + +// GetSelector returns a map of labels to match requests to this Resolver. +func (r *Resolver) GetSelector(context.Context) map[string]string { + return map[string]string{ + common.LabelKeyResolverType: LabelValueBundleResolverType, + } +} + +// Validate ensures reqolution request spec from a request are as expected. +func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + return bundle.ValidateParams(ctx, req.Params) +} + +// Resolve uses the given params to resolve the requested file or resource. +func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) { + return bundle.ResolveRequest(ctx, r.kubeClientSet, req) +} diff --git a/pkg/remoteresolution/resolver/bundle/resolver_test.go b/pkg/remoteresolution/resolver/bundle/resolver_test.go new file mode 100644 index 00000000000..9ce0b25f1d7 --- /dev/null +++ b/pkg/remoteresolution/resolver/bundle/resolver_test.go @@ -0,0 +1,610 @@ +/* + 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 bundle_test + +import ( + "context" + "errors" + "fmt" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/registry" + resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + bundle "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/bundle" + frtesting "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework/testing" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + bundleresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle" + frameworktesting "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" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ktesting "k8s.io/client-go/testing" + "knative.dev/pkg/system" + _ "knative.dev/pkg/system/testing" // Setup system.Namespace() + "sigs.k8s.io/yaml" +) + +const ( + disabledError = "cannot handle resolution request, enable-bundles-resolver feature flag not true" +) + +func TestGetSelector(t *testing.T) { + resolver := bundle.Resolver{} + sel := resolver.GetSelector(context.Background()) + if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != bundle.LabelValueBundleResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidate(t *testing.T) { + resolver := bundle.Resolver{} + + paramsWithTask := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues("bar"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req := v1beta1.ResolutionRequestSpec{Params: paramsWithTask} + if err := resolver.Validate(context.Background(), &req); err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + + paramsWithPipeline := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("pipeline"), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues("bar"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req = v1beta1.ResolutionRequestSpec{Params: paramsWithPipeline} + if err := resolver.Validate(context.Background(), &req); err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } +} + +func TestValidateDisabled(t *testing.T) { + resolver := bundle.Resolver{} + + var err error + + params := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues("bar"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req := v1beta1.ResolutionRequestSpec{Params: params} + err = resolver.Validate(resolverDisabledContext(), &req) + 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 TestValidateMissing(t *testing.T) { + resolver := bundle.Resolver{} + + var err error + + paramsMissingBundle := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req := v1beta1.ResolutionRequestSpec{Params: paramsMissingBundle} + err = resolver.Validate(context.Background(), &req) + if err == nil { + t.Fatalf("expected missing kind err") + } + + paramsMissingName := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues("bar"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req = v1beta1.ResolutionRequestSpec{Params: paramsMissingName} + err = resolver.Validate(context.Background(), &req) + if err == nil { + t.Fatalf("expected missing name err") + } +} + +func TestResolveDisabled(t *testing.T) { + resolver := bundle.Resolver{} + + var err error + + params := []pipelinev1.Param{{ + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues("bar"), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req := v1beta1.ResolutionRequestSpec{Params: params} + _, err = resolver.Resolve(resolverDisabledContext(), &req) + 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 TestResolve_KeyChainError(t *testing.T) { + resolver := &bundle.Resolver{} + params := ¶ms{ + bundle: "foo", + name: "example-task", + kind: "task", + secret: "bar", + } + + ctx, _ := ttesting.SetupFakeContext(t) + request := createRequest(params) + + d := test.Data{ + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: bundleresolution.ConfigMapName, + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: map[string]string{ + bundleresolution.ConfigKind: "task", + }, + }}, + } + + testAssets, cancel := frtesting.GetResolverFrameworkController(ctx, t, d, resolver) + defer cancel() + + expectedErr := apierrors.NewBadRequest("bad request") + // return error when getting secrets from kube client + testAssets.Clients.Kube.Fake.PrependReactor("get", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, nil, expectedErr + }) + + err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, strings.Join([]string{request.Namespace, request.Name}, "/")) + if err == nil { + t.Fatalf("expected to get error but got nothing") + } + + if !errors.Is(err, expectedErr) { + t.Fatalf("expected to get error %v, but got %v", expectedErr, err) + } +} + +type params struct { + secret string + bundle string + name string + kind string +} + +func TestResolve(t *testing.T) { + // example task resource + exampleTask := &pipelinev1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-task", + Namespace: "task-ns", + ResourceVersion: "00002", + }, + TypeMeta: metav1.TypeMeta{ + Kind: string(pipelinev1beta1.NamespacedTaskKind), + APIVersion: "tekton.dev/v1beta1", + }, + Spec: pipelinev1beta1.TaskSpec{ + Steps: []pipelinev1beta1.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) + } + + // example pipeline resource + examplePipeline := &pipelinev1beta1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pipeline", + Namespace: "pipeline-ns", + ResourceVersion: "00001", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Pipeline", + APIVersion: "tekton.dev/v1beta1", + }, + Spec: pipelinev1beta1.PipelineSpec{ + Tasks: []pipelinev1beta1.PipelineTask{{ + Name: "some-pipeline-task", + TaskRef: &pipelinev1beta1.TaskRef{ + Name: "some-task", + Kind: pipelinev1beta1.NamespacedTaskKind, + }, + }}, + }, + } + pipelineAsYAML, err := yaml.Marshal(examplePipeline) + if err != nil { + t.Fatalf("couldn't marshal pipeline: %v", err) + } + + // too many objects in bundle resolver test + var tooManyObjs []runtime.Object + for i := 0; i <= bundleresolution.MaximumBundleObjects; i++ { + name := fmt.Sprintf("%d-task", i) + obj := pipelinev1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "Task", + }, + } + tooManyObjs = append(tooManyObjs, &obj) + } + + // Set up a fake registry to push an image to. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + r := fmt.Sprintf("%s/%s", u.Host, "testbundleresolver") + testImages := map[string]*imageRef{ + "single-task": pushToRegistry(t, r, "single-task", []runtime.Object{exampleTask}, test.DefaultObjectAnnotationMapper), + "single-pipeline": pushToRegistry(t, r, "single-pipeline", []runtime.Object{examplePipeline}, test.DefaultObjectAnnotationMapper), + "multiple-resources": pushToRegistry(t, r, "multiple-resources", []runtime.Object{exampleTask, examplePipeline}, test.DefaultObjectAnnotationMapper), + "too-many-objs": pushToRegistry(t, r, "too-many-objs", tooManyObjs, asIsMapper), + "single-task-no-version": pushToRegistry(t, r, "single-task-no-version", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{Kind: "task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper), + "single-task-no-kind": pushToRegistry(t, r, "single-task-no-kind", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper), + "single-task-no-name": pushToRegistry(t, r, "single-task-no-name", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "task"}}}, asIsMapper), + "single-task-kind-incorrect-form": pushToRegistry(t, r, "single-task-kind-incorrect-form", []runtime.Object{&pipelinev1beta1.Task{TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1beta1", Kind: "Task"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}}, asIsMapper), + } + + testcases := []struct { + name string + args *params + imageName string + kindInBundle string + expectedStatus *v1beta1.ResolutionRequestStatus + expectedErrMessage string + }{ + { + name: "single task: digest is included in the bundle parameter", + args: ¶ms{ + bundle: fmt.Sprintf("%s@%s:%s", testImages["single-task"].uri, testImages["single-task"].algo, testImages["single-task"].hex), + name: "example-task", + kind: "task", + }, + imageName: "single-task", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), + }, { + name: "single task: param kind is capitalized, but kind in bundle is not", + args: ¶ms{ + bundle: fmt.Sprintf("%s@%s:%s", testImages["single-task"].uri, testImages["single-task"].algo, testImages["single-task"].hex), + name: "example-task", + kind: "Task", + }, + kindInBundle: "task", + imageName: "single-task", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), + }, { + name: "single task: tag is included in the bundle parameter", + args: ¶ms{ + bundle: testImages["single-task"].uri + ":latest", + name: "example-task", + kind: "task", + }, + imageName: "single-task", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), + }, { + name: "single task: using default kind value from configmap", + args: ¶ms{ + bundle: testImages["single-task"].uri + ":latest", + name: "example-task", + }, + imageName: "single-task", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), + }, { + name: "single pipeline", + args: ¶ms{ + bundle: testImages["single-pipeline"].uri + ":latest", + name: "example-pipeline", + kind: "pipeline", + }, + imageName: "single-pipeline", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(pipelineAsYAML), + }, { + name: "multiple resources: an image has both task and pipeline resource", + args: ¶ms{ + bundle: testImages["multiple-resources"].uri + ":latest", + name: "example-pipeline", + kind: "pipeline", + }, + imageName: "multiple-resources", + expectedStatus: resolution.CreateResolutionRequestStatusWithData(pipelineAsYAML), + }, { + name: "too many objects in an image", + args: ¶ms{ + bundle: testImages["too-many-objs"].uri + ":latest", + name: "2-task", + kind: "task", + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErrMessage: fmt.Sprintf("contained more than the maximum %d allow objects", bundleresolution.MaximumBundleObjects), + }, { + name: "single task no version", + args: ¶ms{ + bundle: testImages["single-task-no-version"].uri + ":latest", + name: "foo", + kind: "task", + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundleresolution.BundleAnnotationAPIVersion), + }, { + name: "single task no kind", + args: ¶ms{ + bundle: testImages["single-task-no-kind"].uri + ":latest", + name: "foo", + kind: "task", + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundleresolution.BundleAnnotationKind), + }, { + name: "single task no name", + args: ¶ms{ + bundle: testImages["single-task-no-name"].uri + ":latest", + name: "foo", + kind: "task", + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundleresolution.BundleAnnotationName), + }, { + name: "single task kind incorrect form", + args: ¶ms{ + bundle: testImages["single-task-kind-incorrect-form"].uri + ":latest", + name: "foo", + kind: "task", + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErrMessage: fmt.Sprintf("the layer 0 the annotation %s must be lowercased and singular, found %s", bundleresolution.BundleAnnotationKind, "Task"), + }, + } + + resolver := &bundle.Resolver{} + confMap := map[string]string{ + bundleresolution.ConfigKind: "task", + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + + request := createRequest(tc.args) + + d := test.Data{ + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: bundleresolution.ConfigMapName, + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: confMap, + }, { + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + Name: resolverconfig.GetFeatureFlagsConfigName(), + }, + Data: map[string]string{ + "enable-bundles-resolver": "true", + }, + }}, + } + var expectedStatus *v1beta1.ResolutionRequestStatus + var expectedError error + if tc.expectedStatus != nil { + expectedStatus = tc.expectedStatus.DeepCopy() + if tc.expectedErrMessage == "" { + if expectedStatus.Annotations == nil { + expectedStatus.Annotations = make(map[string]string) + } + + switch { + case tc.kindInBundle != "": + expectedStatus.Annotations[bundleresolution.ResolverAnnotationKind] = tc.kindInBundle + case tc.args.kind != "": + expectedStatus.Annotations[bundleresolution.ResolverAnnotationKind] = tc.args.kind + default: + expectedStatus.Annotations[bundleresolution.ResolverAnnotationKind] = "task" + } + + expectedStatus.Annotations[bundleresolution.ResolverAnnotationName] = tc.args.name + expectedStatus.Annotations[bundleresolution.ResolverAnnotationAPIVersion] = "v1beta1" + + expectedStatus.RefSource = &pipelinev1.RefSource{ + URI: testImages[tc.imageName].uri, + Digest: map[string]string{ + testImages[tc.imageName].algo: testImages[tc.imageName].hex, + }, + EntryPoint: tc.args.name, + } + expectedStatus.Source = expectedStatus.RefSource + } else { + expectedError = createError(tc.args.bundle, tc.expectedErrMessage) + expectedStatus.Status.Conditions[0].Message = expectedError.Error() + } + } + + frtesting.RunResolverReconcileTest(ctx, t, d, resolver, request, expectedStatus, expectedError) + }) + } +} + +func createRequest(p *params) *v1beta1.ResolutionRequest { + rr := &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: bundle.LabelValueBundleResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: bundleresolution.ParamBundle, + Value: *pipelinev1.NewStructuredValues(p.bundle), + }, { + Name: bundleresolution.ParamName, + Value: *pipelinev1.NewStructuredValues(p.name), + }, { + Name: bundleresolution.ParamKind, + Value: *pipelinev1.NewStructuredValues(p.kind), + }, { + Name: bundleresolution.ParamImagePullSecret, + Value: *pipelinev1.NewStructuredValues(p.secret), + }}, + }, + } + return rr +} + +func createError(image, msg string) error { + return &resolutioncommon.GetResourceError{ + ResolverName: bundle.BundleResolverName, + Key: "foo/rr", + Original: fmt.Errorf("invalid tekton bundle %s, error: %s", image, msg), + } +} + +func asIsMapper(obj runtime.Object) map[string]string { + annotations := map[string]string{} + if test.GetObjectName(obj) != "" { + annotations[bundleresolution.BundleAnnotationName] = test.GetObjectName(obj) + } + + if obj.GetObjectKind().GroupVersionKind().Kind != "" { + annotations[bundleresolution.BundleAnnotationKind] = obj.GetObjectKind().GroupVersionKind().Kind + } + if obj.GetObjectKind().GroupVersionKind().Version != "" { + annotations[bundleresolution.BundleAnnotationAPIVersion] = obj.GetObjectKind().GroupVersionKind().Version + } + return annotations +} + +func resolverDisabledContext() context.Context { + return frameworktesting.ContextWithBundlesResolverDisabled(context.Background()) +} + +type imageRef struct { + // uri is the image repositry identifier i.e. "gcr.io/tekton-releases/catalog/upstream/golang-build" + uri string + // algo is the algorithm portion of a particular image digest i.e. "sha256". + algo string + // hex is hex encoded portion of a particular image digest i.e. "23293df97dc11957ec36a88c80101bb554039a76e8992a435112eea8283b30d4". + hex string +} + +// pushToRegistry pushes an image to the registry and returns an imageRef. +// It accepts a registry address, image name, the data and an ObjectAnnotationMapper +// to map an object to the annotations for it. +// NOTE: Every image pushed to the registry has a default tag named "latest". +func pushToRegistry(t *testing.T, registry, imageName string, data []runtime.Object, mapper test.ObjectAnnotationMapper) *imageRef { + t.Helper() + ref, err := test.CreateImageWithAnnotations(fmt.Sprintf("%s/%s:latest", registry, imageName), mapper, data...) + if err != nil { + t.Fatalf("couldn't push the image: %v", err) + } + + refSplit := strings.Split(ref, "@") + uri, digest := refSplit[0], refSplit[1] + digSplits := strings.Split(digest, ":") + algo, hex := digSplits[0], digSplits[1] + + return &imageRef{ + uri: uri, + algo: algo, + hex: hex, + } +} diff --git a/pkg/remoteresolution/resolver/cluster/resolver.go b/pkg/remoteresolution/resolver/cluster/resolver.go new file mode 100644 index 00000000000..7c1e9072334 --- /dev/null +++ b/pkg/remoteresolution/resolver/cluster/resolver.go @@ -0,0 +1,87 @@ +/* +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 cluster + +import ( + "context" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + clientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + pipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/cluster" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" +) + +const ( + // 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{} + +// ResolverV2 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, + } +} + +// Validate returns an error if the given parameter map is not +// valid for a resource request targeting the cluster resolver. +func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + return cluster.ValidateParams(ctx, req.Params) +} + +// Resolve performs the work of fetching a resource from a namespace with the given +// parameters. +func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) { + return cluster.ResolveFromParams(ctx, req.Params, r.pipelineClientSet) +} + +var _ resolutionframework.ConfigWatcher = &Resolver{} + +// GetConfigName returns the name of the cluster resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return configMapName +} diff --git a/pkg/remoteresolution/resolver/cluster/resolver_test.go b/pkg/remoteresolution/resolver/cluster/resolver_test.go new file mode 100644 index 00000000000..feecae799c7 --- /dev/null +++ b/pkg/remoteresolution/resolver/cluster/resolver_test.go @@ -0,0 +1,507 @@ +/* + 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 cluster_test + +import ( + "context" + "encoding/base64" + "encoding/hex" + "errors" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + cluster "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/cluster" + frtesting "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework/testing" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + clusterresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/cluster" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + frameworktesting "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" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/system" + _ "knative.dev/pkg/system/testing" + "sigs.k8s.io/yaml" +) + +const ( + disabledError = "cannot handle resolution request, enable-cluster-resolver feature flag not true" +) + +func TestGetSelector(t *testing.T) { + resolver := cluster.Resolver{} + sel := resolver.GetSelector(context.Background()) + if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != cluster.LabelValueClusterResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidate(t *testing.T) { + resolver := cluster.Resolver{} + + params := []pipelinev1.Param{{ + Name: clusterresolution.KindParam, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: clusterresolution.NamespaceParam, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: clusterresolution.NameParam, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + + ctx := framework.InjectResolverConfigToContext(context.Background(), map[string]string{ + clusterresolution.AllowedNamespacesKey: "foo,bar", + clusterresolution.BlockedNamespacesKey: "abc,def", + }) + + req := v1beta1.ResolutionRequestSpec{Params: params} + if err := resolver.Validate(ctx, &req); err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } +} + +func TestValidateNotEnabled(t *testing.T) { + resolver := cluster.Resolver{} + + var err error + + params := []pipelinev1.Param{{ + Name: clusterresolution.KindParam, + Value: *pipelinev1.NewStructuredValues("task"), + }, { + Name: clusterresolution.NamespaceParam, + Value: *pipelinev1.NewStructuredValues("foo"), + }, { + Name: clusterresolution.NameParam, + Value: *pipelinev1.NewStructuredValues("baz"), + }} + req := v1beta1.ResolutionRequestSpec{Params: params} + err = resolver.Validate(resolverDisabledContext(), &req) + 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 TestValidateFailure(t *testing.T) { + testCases := []struct { + name string + params map[string]string + conf map[string]string + expectedErr string + }{ + { + name: "missing kind", + params: map[string]string{ + clusterresolution.NameParam: "foo", + clusterresolution.NamespaceParam: "bar", + }, + expectedErr: "missing required cluster resolver params: kind", + }, { + name: "invalid kind", + params: map[string]string{ + clusterresolution.KindParam: "banana", + clusterresolution.NamespaceParam: "foo", + clusterresolution.NameParam: "bar", + }, + expectedErr: "unknown or unsupported resource kind 'banana'", + }, { + name: "missing multiple", + params: map[string]string{ + clusterresolution.KindParam: "task", + }, + expectedErr: "missing required cluster resolver params: name, namespace", + }, { + name: "not in allowed namespaces", + params: map[string]string{ + clusterresolution.KindParam: "task", + clusterresolution.NamespaceParam: "foo", + clusterresolution.NameParam: "baz", + }, + conf: map[string]string{ + clusterresolution.AllowedNamespacesKey: "abc,def", + }, + expectedErr: "access to specified namespace foo is not allowed", + }, { + name: "in blocked namespaces", + params: map[string]string{ + clusterresolution.KindParam: "task", + clusterresolution.NamespaceParam: "foo", + clusterresolution.NameParam: "baz", + }, + conf: map[string]string{ + clusterresolution.BlockedNamespacesKey: "foo,bar", + }, + expectedErr: "access to specified namespace foo is blocked", + }, + { + name: "blocked by star", + params: map[string]string{ + clusterresolution.KindParam: "task", + clusterresolution.NamespaceParam: "foo", + clusterresolution.NameParam: "baz", + }, + conf: map[string]string{ + clusterresolution.BlockedNamespacesKey: "*", + }, + expectedErr: "only explicit allowed access to namespaces is allowed", + }, + { + name: "blocked by star but allowed explicitly", + params: map[string]string{ + clusterresolution.KindParam: "task", + clusterresolution.NamespaceParam: "foo", + clusterresolution.NameParam: "baz", + }, + conf: map[string]string{ + clusterresolution.BlockedNamespacesKey: "*", + clusterresolution.AllowedNamespacesKey: "foo", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := &cluster.Resolver{} + + ctx := context.Background() + if len(tc.conf) > 0 { + ctx = framework.InjectResolverConfigToContext(ctx, tc.conf) + } + + var asParams []pipelinev1.Param + for k, v := range tc.params { + asParams = append(asParams, pipelinev1.Param{ + Name: k, + Value: *pipelinev1.NewStructuredValues(v), + }) + } + req := v1beta1.ResolutionRequestSpec{Params: asParams} + err := resolver.Validate(ctx, &req) + if tc.expectedErr == "" { + if err != nil { + t.Fatalf("got unexpected error: %v", err) + } + return + } + 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) { + defaultNS := "pipeline-ns" + + exampleTask := &pipelinev1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-task", + Namespace: "task-ns", + ResourceVersion: "00002", + UID: "a123", + }, + TypeMeta: metav1.TypeMeta{ + Kind: string(pipelinev1beta1.NamespacedTaskKind), + APIVersion: "tekton.dev/v1", + }, + Spec: pipelinev1.TaskSpec{ + Steps: []pipelinev1.Step{{ + Name: "some-step", + Image: "some-image", + Command: []string{"something"}, + }}, + }, + } + taskChecksum, err := exampleTask.Checksum() + if err != nil { + t.Fatalf("couldn't checksum task: %v", err) + } + taskAsYAML, err := yaml.Marshal(exampleTask) + if err != nil { + t.Fatalf("couldn't marshal task: %v", err) + } + + examplePipeline := &pipelinev1.Pipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pipeline", + Namespace: defaultNS, + ResourceVersion: "00001", + UID: "b123", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Pipeline", + APIVersion: "tekton.dev/v1", + }, + Spec: pipelinev1.PipelineSpec{ + Tasks: []pipelinev1.PipelineTask{{ + Name: "some-pipeline-task", + TaskRef: &pipelinev1.TaskRef{ + Name: "some-task", + Kind: pipelinev1.NamespacedTaskKind, + }, + }}, + }, + } + pipelineChecksum, err := examplePipeline.Checksum() + if err != nil { + t.Fatalf("couldn't checksum pipeline: %v", err) + } + 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 + allowedNamespaces string + blockedNamespaces string + expectedStatus *v1beta1.ResolutionRequestStatus + expectedErr error + }{ + { + name: "successful task", + kind: "task", + resourceName: exampleTask.Name, + namespace: exampleTask.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(taskAsYAML), + RefSource: &pipelinev1.RefSource{ + URI: "/apis/tekton.dev/v1/namespaces/task-ns/task/example-task@a123", + Digest: map[string]string{ + "sha256": hex.EncodeToString(taskChecksum), + }, + }, + }, + }, + }, { + name: "successful pipeline", + kind: "pipeline", + resourceName: examplePipeline.Name, + namespace: examplePipeline.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(pipelineAsYAML), + RefSource: &pipelinev1.RefSource{ + URI: "/apis/tekton.dev/v1/namespaces/pipeline-ns/pipeline/example-pipeline@b123", + Digest: map[string]string{ + "sha256": hex.EncodeToString(pipelineChecksum), + }, + }, + }, + }, + }, { + name: "default namespace", + kind: "pipeline", + resourceName: examplePipeline.Name, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(pipelineAsYAML), + RefSource: &pipelinev1.RefSource{ + URI: "/apis/tekton.dev/v1/namespaces/pipeline-ns/pipeline/example-pipeline@b123", + Digest: map[string]string{ + "sha256": hex.EncodeToString(pipelineChecksum), + }, + }, + }, + }, + }, { + name: "default kind", + resourceName: exampleTask.Name, + namespace: exampleTask.Namespace, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{}, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString(taskAsYAML), + RefSource: &pipelinev1.RefSource{ + URI: "/apis/tekton.dev/v1/namespaces/task-ns/task/example-task@a123", + Digest: map[string]string{ + "sha256": hex.EncodeToString(taskChecksum), + }, + }, + }, + }, + }, { + name: "no such task", + kind: "task", + resourceName: exampleTask.Name, + namespace: "other-ns", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &resolutioncommon.GetResourceError{ + ResolverName: cluster.ClusterResolverName, + Key: "foo/rr", + Original: errors.New(`tasks.tekton.dev "example-task" not found`), + }, + }, { + name: "not in allowed namespaces", + kind: "task", + resourceName: exampleTask.Name, + namespace: "other-ns", + allowedNamespaces: "foo,bar", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &resolutioncommon.InvalidRequestError{ + ResolutionRequestKey: "foo/rr", + Message: "access to specified namespace other-ns is not allowed", + }, + }, { + name: "in blocked namespaces", + kind: "task", + resourceName: exampleTask.Name, + namespace: "other-ns", + blockedNamespaces: "foo,other-ns,bar", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &resolutioncommon.InvalidRequestError{ + ResolutionRequestKey: "foo/rr", + Message: "access to specified namespace other-ns is blocked", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + + request := createRequest(tc.kind, tc.resourceName, tc.namespace) + + confMap := map[string]string{ + clusterresolution.DefaultKindKey: "task", + clusterresolution.DefaultNamespaceKey: defaultNS, + } + if tc.allowedNamespaces != "" { + confMap[clusterresolution.AllowedNamespacesKey] = tc.allowedNamespaces + } + if tc.blockedNamespaces != "" { + confMap[clusterresolution.BlockedNamespacesKey] = tc.blockedNamespaces + } + + d := test.Data{ + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-resolver-config", + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: confMap, + }, { + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + Name: resolverconfig.GetFeatureFlagsConfigName(), + }, + Data: map[string]string{ + "enable-cluster-resolver": "true", + }, + }}, + Pipelines: []*pipelinev1.Pipeline{examplePipeline}, + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + Tasks: []*pipelinev1.Task{exampleTask}, + } + + resolver := &cluster.Resolver{} + + var expectedStatus *v1beta1.ResolutionRequestStatus + if tc.expectedStatus != nil { + expectedStatus = tc.expectedStatus.DeepCopy() + + if tc.expectedErr == nil { + reqParams := make(map[string]pipelinev1.ParamValue) + for _, p := range request.Spec.Params { + reqParams[p.Name] = p.Value + } + if expectedStatus.Annotations == nil { + expectedStatus.Annotations = make(map[string]string) + } + expectedStatus.Annotations[clusterresolution.ResourceNameAnnotation] = reqParams[clusterresolution.NameParam].StringVal + if reqParams[clusterresolution.NamespaceParam].StringVal != "" { + expectedStatus.Annotations[clusterresolution.ResourceNamespaceAnnotation] = reqParams[clusterresolution.NamespaceParam].StringVal + } else { + expectedStatus.Annotations[clusterresolution.ResourceNamespaceAnnotation] = defaultNS + } + } else { + expectedStatus.Status.Conditions[0].Message = tc.expectedErr.Error() + } + expectedStatus.Source = expectedStatus.RefSource + } + + frtesting.RunResolverReconcileTest(ctx, t, d, resolver, request, expectedStatus, tc.expectedErr) + }) + } +} + +func createRequest(kind, name, namespace string) *v1beta1.ResolutionRequest { + rr := &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: cluster.LabelValueClusterResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: clusterresolution.NameParam, + Value: *pipelinev1.NewStructuredValues(name), + }}, + }, + } + if kind != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: clusterresolution.KindParam, + Value: *pipelinev1.NewStructuredValues(kind), + }) + } + if namespace != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: clusterresolution.NamespaceParam, + Value: *pipelinev1.NewStructuredValues(namespace), + }) + } + + return rr +} + +func resolverDisabledContext() context.Context { + return frameworktesting.ContextWithClusterResolverDisabled(context.Background()) +} diff --git a/pkg/remoteresolution/resolver/doc.go b/pkg/remoteresolution/resolver/doc.go new file mode 100644 index 00000000000..9dfffc7275d --- /dev/null +++ b/pkg/remoteresolution/resolver/doc.go @@ -0,0 +1,25 @@ +/* +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 resolver contains the upgraded remote resolution framework. +It contains the upgraded framework and the built-in resolves. +It is equivalent to `pkg/resolution/resolver`. +This was necessary to ensure backwards compatibility with the existing framework. + +This framework is `ALPHA` and subject to further refactoring and changes. +*/ +package resolver diff --git a/pkg/remoteresolution/resolver/framework/controller.go b/pkg/remoteresolution/resolver/framework/controller.go new file mode 100644 index 00000000000..665ecd89e93 --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/controller.go @@ -0,0 +1,124 @@ +/* +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 framework + +import ( + "context" + "strings" + + rrclient "github.com/tektoncd/pipeline/pkg/client/resolution/injection/client" + rrinformer "github.com/tektoncd/pipeline/pkg/client/resolution/injection/informers/resolution/v1beta1/resolutionrequest" + framework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "k8s.io/client-go/tools/cache" + "k8s.io/utils/clock" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" +) + +// ReconcilerModifier is a func that can access and modify a reconciler +// in the moments before a resolver is started. It allows for +// things like injecting a test clock. +type ReconcilerModifier = func(reconciler *Reconciler) + +// NewController returns a knative controller for a Tekton Resolver. +// This sets up a lot of the boilerplate that individual resolvers +// shouldn't need to be concerned with since it's common to all of them. +func NewController(ctx context.Context, resolver Resolver, modifiers ...ReconcilerModifier) func(context.Context, configmap.Watcher) *controller.Impl { + if err := framework.ValidateResolver(ctx, resolver.GetSelector(ctx)); err != nil { + panic(err.Error()) + } + return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl { + logger := logging.FromContext(ctx) + kubeclientset := kubeclient.Get(ctx) + rrclientset := rrclient.Get(ctx) + rrInformer := rrinformer.Get(ctx) + + if err := resolver.Initialize(ctx); err != nil { + panic(err.Error()) + } + + r := &Reconciler{ + LeaderAwareFuncs: framework.LeaderAwareFuncs(rrInformer.Lister()), + kubeClientSet: kubeclientset, + resolutionRequestLister: rrInformer.Lister(), + resolutionRequestClientSet: rrclientset, + resolver: resolver, + } + + watchConfigChanges(ctx, r, cmw) + + // TODO(sbwsg): Do better sanitize. + resolverName := resolver.GetName(ctx) + resolverName = strings.ReplaceAll(resolverName, "/", "") + resolverName = strings.ReplaceAll(resolverName, " ", "") + + applyModifiersAndDefaults(ctx, r, modifiers) + + impl := controller.NewContext(ctx, r, controller.ControllerOptions{ + WorkQueueName: "TektonResolverFramework." + resolverName, + Logger: logger, + }) + + _, err := rrInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: framework.FilterResolutionRequestsBySelector(resolver.GetSelector(ctx)), + Handler: cache.ResourceEventHandlerFuncs{ + AddFunc: impl.Enqueue, + UpdateFunc: func(oldObj, newObj interface{}) { + impl.Enqueue(newObj) + }, + // TODO(sbwsg): should we deliver delete events + // to the resolver? + // DeleteFunc: impl.Enqueue, + }, + }) + if err != nil { + logging.FromContext(ctx).Panicf("Couldn't register ResolutionRequest informer event handler: %w", err) + } + + return impl + } +} + +// watchConfigChanges binds a framework.Resolver to updates on its +// configmap, using knative's configmap helpers. This is only done if +// the resolver implements the framework.ConfigWatcher interface. +func watchConfigChanges(ctx context.Context, reconciler *Reconciler, cmw configmap.Watcher) { + if configWatcher, ok := reconciler.resolver.(framework.ConfigWatcher); ok { + logger := logging.FromContext(ctx) + resolverConfigName := configWatcher.GetConfigName(ctx) + if resolverConfigName == "" { + panic("resolver returned empty config name") + } + reconciler.configStore = framework.NewConfigStore(resolverConfigName, logger) + reconciler.configStore.WatchConfigs(cmw) + } +} + +// applyModifiersAndDefaults applies the given modifiers to +// a reconciler and, after doing so, sets any default values for things +// that weren't set by a modifier. +func applyModifiersAndDefaults(ctx context.Context, r *Reconciler, modifiers []ReconcilerModifier) { + for _, mod := range modifiers { + mod(r) + } + + if r.Clock == nil { + r.Clock = clock.RealClock{} + } +} diff --git a/pkg/remoteresolution/resolver/framework/doc.go b/pkg/remoteresolution/resolver/framework/doc.go new file mode 100644 index 00000000000..881c44026ee --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/doc.go @@ -0,0 +1,24 @@ +/* +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 framework contains the upgraded remote resolution framework. +It is equivalent to `pkg/resolution/resolver/framework`. +This was necessary to ensure backwards compatibility with the existing framework. + +This framework is `ALPHA` and subject to further refactoring and changes. +*/ +package framework diff --git a/pkg/remoteresolution/resolver/framework/fakeresolver.go b/pkg/remoteresolution/resolver/framework/fakeresolver.go new file mode 100644 index 00000000000..995ead2e19e --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/fakeresolver.go @@ -0,0 +1,73 @@ +/* + 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 framework + +import ( + "context" + "time" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" +) + +var _ Resolver = &FakeResolver{} + +// FakeResolver implements a framework.Resolver that can fetch pre-configured strings based on a parameter value, or return +// resolution attempts with a configured error. +type FakeResolver framework.FakeResolver + +// Initialize performs any setup required by the fake resolver. +func (r *FakeResolver) Initialize(ctx context.Context) error { + if r.ForParam == nil { + r.ForParam = make(map[string]*framework.FakeResolvedResource) + } + return nil +} + +// GetName returns the string name that the fake resolver should be +// associated with. +func (r *FakeResolver) GetName(_ context.Context) string { + return framework.FakeResolverName +} + +// GetSelector returns the labels that resource requests are required to have for +// the fake resolver to process them. +func (r *FakeResolver) GetSelector(_ context.Context) map[string]string { + return map[string]string{ + resolutioncommon.LabelKeyResolverType: framework.LabelValueFakeResolverType, + } +} + +// Validate returns an error if the given parameter map is not +// valid for a resource request targeting the fake resolver. +func (r *FakeResolver) Validate(_ context.Context, req *v1beta1.ResolutionRequestSpec) error { + return framework.ValidateParams(req.Params) +} + +// Resolve performs the work of fetching a file from the fake resolver given a map of +// parameters. +func (r *FakeResolver) Resolve(_ context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) { + return framework.Resolve(req.Params, r.ForParam) +} + +var _ framework.TimedResolution = &FakeResolver{} + +// GetResolutionTimeout returns the configured timeout for the reconciler, or the default time.Duration if not configured. +func (r *FakeResolver) GetResolutionTimeout(ctx context.Context, defaultTimeout time.Duration) time.Duration { + return framework.GetResolutionTimeout(r.Timeout, defaultTimeout) +} diff --git a/pkg/remoteresolution/resolver/framework/interface.go b/pkg/remoteresolution/resolver/framework/interface.go new file mode 100644 index 00000000000..53cc9443143 --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/interface.go @@ -0,0 +1,53 @@ +/* +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 framework + +import ( + "context" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" +) + +// Resolver is the interface to implement for type-specific resource +// resolution. It fetches resources from a given type of remote location +// and returns their content along with any associated annotations. +type Resolver interface { + // Initialize is called at the moment the resolver controller is + // instantiated and is a good place to setup things like + // resource listers. + Initialize(ctx context.Context) error + + // GetName should give back the name of the resolver. E.g. "Git" + GetName(ctx context.Context) string + + // GetSelector returns the labels that are used to direct resolution + // requests to this resolver. + GetSelector(ctx context.Context) map[string]string + + // Validate is given the ressolution request spec + // should return an error if the resolver cannot resolve it. + Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error + + // ResolveRequest receives the resolution request spec + // and returns the resolved data along with any annotations + // to include in the response. If resolution fails then an error + // should be returned instead. If a resolution.Error + // is returned then its Reason and Message are used as part of the + // response to the request. + Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) +} diff --git a/pkg/remoteresolution/resolver/framework/reconciler.go b/pkg/remoteresolution/resolver/framework/reconciler.go new file mode 100644 index 00000000000..c4fb6177075 --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/reconciler.go @@ -0,0 +1,230 @@ +/* +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 framework + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "time" + + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + rrclient "github.com/tektoncd/pipeline/pkg/client/resolution/clientset/versioned" + rrv1beta1 "github.com/tektoncd/pipeline/pkg/client/resolution/listers/resolution/v1beta1" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/utils/clock" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + "knative.dev/pkg/reconciler" +) + +// defaultMaximumResolutionDuration is the maximum amount of time +// resolution may take. + +// defaultMaximumResolutionDuration is the max time that a call to +// Resolve() may take. It can be overridden by a resolver implementing +// the framework.TimedResolution interface. +const defaultMaximumResolutionDuration = time.Minute + +// statusDataPatch is the json structure that will be PATCHed into +// a ResolutionRequest with its data and annotations once successfully +// resolved. +type statusDataPatch struct { + Annotations map[string]string `json:"annotations"` + Data string `json:"data"` + Source *pipelinev1beta1.ConfigSource `json:"source"` + RefSource *pipelinev1.RefSource `json:"refSource"` +} + +// Reconciler handles ResolutionRequest objects, performs functionality +// common to all resolvers and delegates resolver-specific actions +// to its embedded type-specific Resolver object. +type Reconciler struct { + // Implements reconciler.LeaderAware + reconciler.LeaderAwareFuncs + + // Clock is used by the reconciler to track the passage of time + // and can be overridden for tests. + Clock clock.PassiveClock + + resolver Resolver + kubeClientSet kubernetes.Interface + resolutionRequestLister rrv1beta1.ResolutionRequestLister + resolutionRequestClientSet rrclient.Interface + + configStore *framework.ConfigStore +} + +var _ reconciler.LeaderAware = &Reconciler{} + +// Reconcile receives the string key of a ResolutionRequest object, looks +// it up, checks it for common errors, and then delegates +// resolver-specific functionality to the reconciler's embedded +// type-specific resolver. Any errors that occur during validation or +// resolution are handled by updating or failing the ResolutionRequest. +func (r *Reconciler) Reconcile(ctx context.Context, key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + err = &resolutioncommon.InvalidResourceKeyError{Key: key, Original: err} + return controller.NewPermanentError(err) + } + + rr, err := r.resolutionRequestLister.ResolutionRequests(namespace).Get(name) + if err != nil { + err := &resolutioncommon.GetResourceError{ResolverName: "resolutionrequest", Key: key, Original: err} + return controller.NewPermanentError(err) + } + + if rr.IsDone() { + return nil + } + + // Inject request-scoped information into the context, such as + // the namespace that the request originates from and the + // configuration from the configmap this resolver is watching. + ctx = resolutioncommon.InjectRequestNamespace(ctx, namespace) + ctx = resolutioncommon.InjectRequestName(ctx, name) + if r.configStore != nil { + ctx = r.configStore.ToContext(ctx) + } + + return r.resolve(ctx, key, rr) +} + +func (r *Reconciler) resolve(ctx context.Context, key string, rr *v1beta1.ResolutionRequest) error { + errChan := make(chan error) + resourceChan := make(chan framework.ResolvedResource) + + timeoutDuration := defaultMaximumResolutionDuration + if timed, ok := r.resolver.(framework.TimedResolution); ok { + timeoutDuration = timed.GetResolutionTimeout(ctx, defaultMaximumResolutionDuration) + } + + // A new context is created for resolution so that timeouts can + // be enforced without affecting other uses of ctx (e.g. sending + // Updates to ResolutionRequest objects). + resolutionCtx, cancelFn := context.WithTimeout(ctx, timeoutDuration) + defer cancelFn() + + go func() { + validationError := r.resolver.Validate(resolutionCtx, &rr.Spec) + if validationError != nil { + errChan <- &resolutioncommon.InvalidRequestError{ + ResolutionRequestKey: key, + Message: validationError.Error(), + } + return + } + resource, resolveErr := r.resolver.Resolve(resolutionCtx, &rr.Spec) + if resolveErr != nil { + errChan <- &resolutioncommon.GetResourceError{ + ResolverName: r.resolver.GetName(resolutionCtx), + Key: key, + Original: resolveErr, + } + return + } + resourceChan <- resource + }() + + select { + case err := <-errChan: + if err != nil { + return r.OnError(ctx, rr, err) + } + case <-resolutionCtx.Done(): + if err := resolutionCtx.Err(); err != nil { + return r.OnError(ctx, rr, err) + } + case resource := <-resourceChan: + return r.writeResolvedData(ctx, rr, resource) + } + + return errors.New("unknown error") +} + +// OnError is used to handle any situation where a ResolutionRequest has +// reached a terminal situation that cannot be recovered from. +func (r *Reconciler) OnError(ctx context.Context, rr *v1beta1.ResolutionRequest, err error) error { + if rr == nil { + return controller.NewPermanentError(err) + } + if err != nil { + _ = r.MarkFailed(ctx, rr, err) + return controller.NewPermanentError(err) + } + return nil +} + +// MarkFailed updates a ResolutionRequest as having failed. It returns +// errors that occur during the update process or nil if the update +// appeared to succeed. +func (r *Reconciler) MarkFailed(ctx context.Context, rr *v1beta1.ResolutionRequest, resolutionErr error) error { + key := fmt.Sprintf("%s/%s", rr.Namespace, rr.Name) + reason, resolutionErr := resolutioncommon.ReasonError(resolutionErr) + latestGeneration, err := r.resolutionRequestClientSet.ResolutionV1beta1().ResolutionRequests(rr.Namespace).Get(ctx, rr.Name, metav1.GetOptions{}) + if err != nil { + logging.FromContext(ctx).Warnf("error getting latest generation of resolutionrequest %q: %v", key, err) + return err + } + if latestGeneration.IsDone() { + return nil + } + latestGeneration.Status.MarkFailed(reason, resolutionErr.Error()) + _, err = r.resolutionRequestClientSet.ResolutionV1beta1().ResolutionRequests(rr.Namespace).UpdateStatus(ctx, latestGeneration, metav1.UpdateOptions{}) + if err != nil { + logging.FromContext(ctx).Warnf("error marking resolutionrequest %q as failed: %v", key, err) + return err + } + return nil +} + +func (r *Reconciler) writeResolvedData(ctx context.Context, rr *v1beta1.ResolutionRequest, resource framework.ResolvedResource) error { + encodedData := base64.StdEncoding.Strict().EncodeToString(resource.Data()) + patchBytes, err := json.Marshal(map[string]statusDataPatch{ + "status": { + Data: encodedData, + Annotations: resource.Annotations(), + RefSource: resource.RefSource(), + Source: (*pipelinev1beta1.ConfigSource)(resource.RefSource()), + }, + }) + if err != nil { + return r.OnError(ctx, rr, &resolutioncommon.UpdatingRequestError{ + ResolutionRequestKey: fmt.Sprintf("%s/%s", rr.Namespace, rr.Name), + Original: fmt.Errorf("error serializing resource request patch: %w", err), + }) + } + _, err = r.resolutionRequestClientSet.ResolutionV1beta1().ResolutionRequests(rr.Namespace).Patch(ctx, rr.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, "status") + if err != nil { + return r.OnError(ctx, rr, &resolutioncommon.UpdatingRequestError{ + ResolutionRequestKey: fmt.Sprintf("%s/%s", rr.Namespace, rr.Name), + Original: err, + }) + } + + return nil +} diff --git a/pkg/remoteresolution/resolver/framework/reconciler_test.go b/pkg/remoteresolution/resolver/framework/reconciler_test.go new file mode 100644 index 00000000000..e72582ca471 --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/reconciler_test.go @@ -0,0 +1,290 @@ +/* + 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 framework_test + +import ( + "context" + "encoding/base64" + "errors" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "github.com/tektoncd/pipeline/test" + "github.com/tektoncd/pipeline/test/diff" + "github.com/tektoncd/pipeline/test/names" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + clock "k8s.io/utils/clock/testing" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" + cminformer "knative.dev/pkg/configmap/informer" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + "knative.dev/pkg/system" + _ "knative.dev/pkg/system/testing" // Setup system.Namespace() +) + +var ( + now = time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + testClock = clock.NewFakePassiveClock(now) + ignoreLastTransitionTime = cmpopts.IgnoreFields(apis.Condition{}, "LastTransitionTime.Inner.Time") +) + +func TestReconcile(t *testing.T) { + testCases := []struct { + name string + inputRequest *v1beta1.ResolutionRequest + paramMap map[string]*resolutionframework.FakeResolvedResource + reconcilerTimeout time.Duration + expectedStatus *v1beta1.ResolutionRequestStatus + expectedErr error + }{ + { + name: "unknown value", + inputRequest: &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: resolutionframework.LabelValueFakeResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: resolutionframework.FakeParamName, + Value: *pipelinev1.NewStructuredValues("bar"), + }}, + }, + Status: v1beta1.ResolutionRequestStatus{}, + }, + expectedErr: errors.New("error getting \"Fake\" \"foo/rr\": couldn't find resource for param value bar"), + }, { + name: "known value", + inputRequest: &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: resolutionframework.LabelValueFakeResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: resolutionframework.FakeParamName, + Value: *pipelinev1.NewStructuredValues("bar"), + }}, + }, + Status: v1beta1.ResolutionRequestStatus{}, + }, + paramMap: map[string]*resolutionframework.FakeResolvedResource{ + "bar": { + Content: "some content", + AnnotationMap: map[string]string{"foo": "bar"}, + ContentSource: &pipelinev1.RefSource{ + URI: "https://abc.com", + Digest: map[string]string{ + "sha1": "xyz", + }, + EntryPoint: "foo/bar", + }, + }, + }, + expectedStatus: &v1beta1.ResolutionRequestStatus{ + Status: duckv1.Status{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + ResolutionRequestStatusFields: v1beta1.ResolutionRequestStatusFields{ + Data: base64.StdEncoding.Strict().EncodeToString([]byte("some content")), + RefSource: &pipelinev1.RefSource{ + URI: "https://abc.com", + Digest: map[string]string{ + "sha1": "xyz", + }, + EntryPoint: "foo/bar", + }, + Source: &pipelinev1.RefSource{ + URI: "https://abc.com", + Digest: map[string]string{ + "sha1": "xyz", + }, + EntryPoint: "foo/bar", + }, + }, + }, + }, { + name: "error resolving", + inputRequest: &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: resolutionframework.LabelValueFakeResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: resolutionframework.FakeParamName, + Value: *pipelinev1.NewStructuredValues("bar"), + }}, + }, + Status: v1beta1.ResolutionRequestStatus{}, + }, + paramMap: map[string]*resolutionframework.FakeResolvedResource{ + "bar": { + ErrorWith: "fake failure", + }, + }, + expectedErr: errors.New(`error getting "Fake" "foo/rr": fake failure`), + }, { + name: "timeout", + inputRequest: &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now().Add(-59 * time.Second)}, // 1 second before default timeout + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: resolutionframework.LabelValueFakeResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: resolutionframework.FakeParamName, + Value: *pipelinev1.NewStructuredValues("bar"), + }}, + }, + Status: v1beta1.ResolutionRequestStatus{}, + }, + paramMap: map[string]*resolutionframework.FakeResolvedResource{ + "bar": { + WaitFor: 1100 * time.Millisecond, + }, + }, + reconcilerTimeout: 1 * time.Second, + expectedErr: errors.New("context deadline exceeded"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := test.Data{ + ResolutionRequests: []*v1beta1.ResolutionRequest{tc.inputRequest}, + } + + fakeResolver := &framework.FakeResolver{ForParam: tc.paramMap} + if tc.reconcilerTimeout > 0 { + fakeResolver.Timeout = tc.reconcilerTimeout + } + + ctx, _ := ttesting.SetupFakeContext(t) + testAssets, cancel := getResolverFrameworkController(ctx, t, d, fakeResolver, setClockOnReconciler) + defer cancel() + + err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRequestName(tc.inputRequest)) + if tc.expectedErr != nil { + if err == nil { + t.Fatalf("expected to get error %v, but got nothing", tc.expectedErr) + } + if tc.expectedErr.Error() != err.Error() { + t.Fatalf("expected to get error %v, but got %v", tc.expectedErr, err) + } + } else { + if err != nil { + if ok, _ := controller.IsRequeueKey(err); !ok { + t.Fatalf("did not expect an error, but got %v", err) + } + } + + c := testAssets.Clients.ResolutionRequests.ResolutionV1beta1() + reconciledRR, err := c.ResolutionRequests(tc.inputRequest.Namespace).Get(testAssets.Ctx, tc.inputRequest.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("getting updated ResolutionRequest: %v", err) + } + if d := cmp.Diff(*tc.expectedStatus, reconciledRR.Status, ignoreLastTransitionTime); d != "" { + t.Errorf("ResolutionRequest status doesn't match %s", diff.PrintWantGot(d)) + } + } + }) + } +} + +func getResolverFrameworkController(ctx context.Context, t *testing.T, d test.Data, resolver framework.Resolver, modifiers ...framework.ReconcilerModifier) (test.Assets, func()) { + t.Helper() + names.TestingSeed() + + ctx, cancel := context.WithCancel(ctx) + c, informers := test.SeedTestData(t, ctx, d) + configMapWatcher := cminformer.NewInformedWatcher(c.Kube, system.Namespace()) + ctl := framework.NewController(ctx, resolver, modifiers...)(ctx, configMapWatcher) + if err := configMapWatcher.Start(ctx.Done()); err != nil { + t.Fatalf("error starting configmap watcher: %v", err) + } + + if la, ok := ctl.Reconciler.(pkgreconciler.LeaderAware); ok { + _ = la.Promote(pkgreconciler.UniversalBucket(), func(pkgreconciler.Bucket, types.NamespacedName) {}) + } + + return test.Assets{ + Logger: logging.FromContext(ctx), + Controller: ctl, + Clients: c, + Informers: informers, + Recorder: controller.GetEventRecorder(ctx).(*record.FakeRecorder), + Ctx: ctx, + }, cancel +} + +func getRequestName(rr *v1beta1.ResolutionRequest) string { + return strings.Join([]string{rr.Namespace, rr.Name}, "/") +} + +func setClockOnReconciler(r *framework.Reconciler) { + if r.Clock == nil { + r.Clock = testClock + } +} diff --git a/pkg/remoteresolution/resolver/framework/testing/fakecontroller.go b/pkg/remoteresolution/resolver/framework/testing/fakecontroller.go new file mode 100644 index 00000000000..eefee4263da --- /dev/null +++ b/pkg/remoteresolution/resolver/framework/testing/fakecontroller.go @@ -0,0 +1,171 @@ +/* + 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 testing + +import ( + "context" + "encoding/base64" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/test" + "github.com/tektoncd/pipeline/test/diff" + "github.com/tektoncd/pipeline/test/names" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + testclock "k8s.io/utils/clock/testing" + "knative.dev/pkg/apis" + cminformer "knative.dev/pkg/configmap/informer" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + "knative.dev/pkg/system" +) + +var ( + now = time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + testClock = testclock.NewFakePassiveClock(now) + ignoreLastTransitionTime = cmpopts.IgnoreFields(apis.Condition{}, "LastTransitionTime.Inner.Time") +) + +// ResolverReconcileTestModifier is a function thaat will be invoked after the test assets and controller have been created +type ResolverReconcileTestModifier = func(resolver framework.Resolver, testAssets test.Assets) + +// RunResolverReconcileTest takes data to seed clients and informers, a Resolver, a ResolutionRequest, and the expected +// ResolutionRequestStatus and error, both of which can be nil. It instantiates a controller for that resolver and +// 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 *v1beta1.ResolutionRequest, + expectedStatus *v1beta1.ResolutionRequestStatus, expectedErr error, resolverModifiers ...ResolverReconcileTestModifier) { + 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)) //nolint + if expectedErr != nil { + if err == nil { + t.Fatalf("expected to get error: `%v`, but got nothing", expectedErr) + } + if expectedErr.Error() != err.Error() { + t.Fatalf("expected to get error `%v`, but got `%v`", expectedErr, err) + } + } else if err != nil { + if ok, _ := controller.IsRequeueKey(err); !ok { + t.Fatalf("did not expect an error, but got `%v`", err) + } + } + + c := testAssets.Clients.ResolutionRequests.ResolutionV1beta1() + reconciledRR, err := c.ResolutionRequests(request.Namespace).Get(testAssets.Ctx, request.Name, metav1.GetOptions{}) //nolint + if err != nil { + t.Fatalf("getting updated ResolutionRequest: %v", err) + } + if expectedStatus != nil { + if d := cmp.Diff(*expectedStatus, reconciledRR.Status, ignoreLastTransitionTime); d != "" { + t.Errorf("ResolutionRequest status doesn't match %s", diff.PrintWantGot(d)) + if expectedStatus.Data != "" && expectedStatus.Data != reconciledRR.Status.Data { + decodedExpectedData, err := base64.StdEncoding.Strict().DecodeString(expectedStatus.Data) + if err != nil { + t.Errorf("couldn't decode expected data: %v", err) + return + } + decodedGotData, err := base64.StdEncoding.Strict().DecodeString(reconciledRR.Status.Data) + if err != nil { + t.Errorf("couldn't decode reconciled data: %v", err) + return + } + if d := cmp.Diff(decodedExpectedData, decodedGotData); d != "" { + t.Errorf("decoded data did not match expected: %s", diff.PrintWantGot(d)) + } + } + } + } +} + +// GetResolverFrameworkController returns an instance of the resolver framework controller/reconciler using the given resolver, +// seeded with d, where d represents the state of the system (existing resources) needed for the test. +func GetResolverFrameworkController(ctx context.Context, t *testing.T, d test.Data, resolver framework.Resolver, modifiers ...framework.ReconcilerModifier) (test.Assets, func()) { + t.Helper() + names.TestingSeed() + return initializeResolverFrameworkControllerAssets(ctx, t, d, resolver, modifiers...) +} + +func initializeResolverFrameworkControllerAssets(ctx context.Context, t *testing.T, d test.Data, resolver framework.Resolver, modifiers ...framework.ReconcilerModifier) (test.Assets, func()) { + t.Helper() + ctx, cancel := context.WithCancel(ctx) + ensureConfigurationConfigMapsExist(&d) + c, informers := test.SeedTestData(t, ctx, d) + configMapWatcher := cminformer.NewInformedWatcher(c.Kube, resolverconfig.ResolversNamespace(system.Namespace())) + ctl := framework.NewController(ctx, resolver, modifiers...)(ctx, configMapWatcher) + if err := configMapWatcher.Start(ctx.Done()); err != nil { + t.Fatalf("error starting configmap watcher: %v", err) + } + + if la, ok := ctl.Reconciler.(pkgreconciler.LeaderAware); ok { + _ = la.Promote(pkgreconciler.UniversalBucket(), func(pkgreconciler.Bucket, types.NamespacedName) {}) + } + + return test.Assets{ + Logger: logging.FromContext(ctx), + Controller: ctl, + Clients: c, + Informers: informers, + Recorder: controller.GetEventRecorder(ctx).(*record.FakeRecorder), + Ctx: ctx, + }, cancel +} + +func getRequestName(rr *v1beta1.ResolutionRequest) string { + return strings.Join([]string{rr.Namespace, rr.Name}, "/") +} + +func setClockOnReconciler(r *framework.Reconciler) { + if r.Clock == nil { + r.Clock = testClock + } +} + +func ensureConfigurationConfigMapsExist(d *test.Data) { + var featureFlagsExists bool + for _, cm := range d.ConfigMaps { + if cm.Name == resolverconfig.GetFeatureFlagsConfigName() { + featureFlagsExists = true + } + } + if !featureFlagsExists { + d.ConfigMaps = append(d.ConfigMaps, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: resolverconfig.GetFeatureFlagsConfigName(), + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: map[string]string{}, + }) + } +} diff --git a/pkg/remoteresolution/resolver/git/resolver.go b/pkg/remoteresolution/resolver/git/resolver.go new file mode 100644 index 00000000000..8aa15b65bd0 --- /dev/null +++ b/pkg/remoteresolution/resolver/git/resolver.go @@ -0,0 +1,145 @@ +/* +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 git + +import ( + "context" + "errors" + "time" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/factory" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/git" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/client-go/kubernetes" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/pkg/logging" +) + +const ( + disabledError = "cannot handle resolution request, enable-git-resolver feature flag not true" + + // labelValueGitResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + labelValueGitResolverType string = "git" + + // gitResolverName is the name that the git resolver should be + // associated with + gitResolverName string = "Git" + + // ConfigMapName is the git resolver's config map + ConfigMapName = "git-resolver-config" + + // cacheSize is the size of the LRU secrets cache + cacheSize = 1024 + // ttl is the time to live for a cache entry + ttl = 5 * time.Minute +) + +var _ framework.Resolver = &Resolver{} + +// Resolver implements a framework.Resolver that can fetch files from git. +type Resolver struct { + kubeClient kubernetes.Interface + logger *zap.SugaredLogger + cache *cache.LRUExpireCache + ttl time.Duration + + // Used in testing + clientFunc func(string, string, string, ...factory.ClientOptionFunc) (*scm.Client, error) +} + +// Initialize performs any setup required by the gitresolver. +func (r *Resolver) Initialize(ctx context.Context) error { + r.kubeClient = kubeclient.Get(ctx) + r.logger = logging.FromContext(ctx) + r.cache = cache.NewLRUExpireCache(cacheSize) + r.ttl = ttl + if r.clientFunc == nil { + r.clientFunc = factory.NewClient + } + return nil +} + +// GetName returns the string name that the gitresolver should be +// associated with. +func (r *Resolver) GetName(_ context.Context) string { + return gitResolverName +} + +// GetSelector returns the labels that resource requests are required to have for +// the gitresolver to process them. +func (r *Resolver) GetSelector(_ context.Context) map[string]string { + return map[string]string{ + resolutioncommon.LabelKeyResolverType: labelValueGitResolverType, + } +} + +// ValidateParams returns an error if the given parameter map is not +// valid for a resource request targeting the gitresolver. +func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + return git.ValidateParams(ctx, req.Params) +} + +// Resolve performs the work of fetching a file from git given a map of +// parameters. +func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) { + origParams := req.Params + + if git.IsDisabled(ctx) { + return nil, errors.New(disabledError) + } + + params, err := git.PopulateDefaultParams(ctx, origParams) + if err != nil { + return nil, err + } + + if params[git.UrlParam] != "" { + return git.ResolveAnonymousGit(ctx, params) + } + + return git.ResolveAPIGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl, r.clientFunc) +} + +var _ resolutionframework.ConfigWatcher = &Resolver{} + +// GetConfigName returns the name of the git resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return ConfigMapName +} + +var _ resolutionframework.TimedResolution = &Resolver{} + +// GetResolutionTimeout returns a time.Duration for the amount of time a +// single git fetch may take. This can be configured with the +// fetch-timeout field in the git-resolver-config configmap. +func (r *Resolver) GetResolutionTimeout(ctx context.Context, defaultTimeout time.Duration) time.Duration { + conf := resolutionframework.GetResolverConfigFromContext(ctx) + if timeoutString, ok := conf[git.DefaultTimeoutKey]; ok { + timeout, err := time.ParseDuration(timeoutString) + if err == nil { + return timeout + } + } + return defaultTimeout +} diff --git a/pkg/remoteresolution/resolver/git/resolver_test.go b/pkg/remoteresolution/resolver/git/resolver_test.go new file mode 100644 index 00000000000..18c3e629e70 --- /dev/null +++ b/pkg/remoteresolution/resolver/git/resolver_test.go @@ -0,0 +1,908 @@ +/* +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 git + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/google/go-cmp/cmp" + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/fake" + "github.com/jenkins-x/go-scm/scm/factory" + resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + frtesting "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework/testing" + common "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + frameworktesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" + gitresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/git" + "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/system" + _ "knative.dev/pkg/system/testing" +) + +func TestGetSelector(t *testing.T) { + resolver := Resolver{} + sel := resolver.GetSelector(context.Background()) + if typ, has := sel[common.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != labelValueGitResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidateParams(t *testing.T) { + tests := []struct { + name string + wantErr string + params map[string]string + }{ + { + name: "params with revision", + params: map[string]string{ + gitresolution.UrlParam: "http://foo/bar/hello/moto", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "https url", + params: map[string]string{ + gitresolution.UrlParam: "https://foo/bar/hello/moto", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "https url with username password", + params: map[string]string{ + gitresolution.UrlParam: "https://user:pass@foo/bar/hello/moto", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "git server url", + params: map[string]string{ + gitresolution.UrlParam: "git://repo/hello/moto", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "git url from a local repository", + params: map[string]string{ + gitresolution.UrlParam: "/tmp/repo", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "git url from a git ssh repository", + params: map[string]string{ + gitresolution.UrlParam: "git@host.com:foo/bar", + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + }, + }, + { + name: "bad url", + params: map[string]string{ + gitresolution.UrlParam: "foo://bar", + gitresolution.PathParam: "path", + gitresolution.RevisionParam: "revision", + }, + wantErr: "invalid git repository url: foo://bar", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := Resolver{} + err := resolver.Validate(context.Background(), &v1beta1.ResolutionRequestSpec{Params: toParams(tt.params)}) + if tt.wantErr == "" { + if err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + return + } + + if d := cmp.Diff(tt.wantErr, err.Error()); d != "" { + t.Errorf("unexpected error: %s", diff.PrintWantGot(d)) + } + }) + } +} + +func TestValidateParamsNotEnabled(t *testing.T) { + resolver := Resolver{} + + var err error + + someParams := map[string]string{ + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + } + err = resolver.Validate(resolverDisabledContext(), &v1beta1.ResolutionRequestSpec{Params: toParams(someParams)}) + 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 TestValidateParams_Failure(t *testing.T) { + testCases := []struct { + name string + params map[string]string + expectedErr string + }{ + { + name: "missing multiple", + params: map[string]string{ + gitresolution.OrgParam: "abcd1234", + gitresolution.RepoParam: "foo", + }, + expectedErr: fmt.Sprintf("missing required git resolver params: %s, %s", gitresolution.RevisionParam, gitresolution.PathParam), + }, { + name: "no repo or url", + params: map[string]string{ + gitresolution.RevisionParam: "abcd1234", + gitresolution.PathParam: "/foo/bar", + }, + expectedErr: "must specify one of 'url' or 'repo'", + }, { + name: "both repo and url", + params: map[string]string{ + gitresolution.RevisionParam: "abcd1234", + gitresolution.PathParam: "/foo/bar", + gitresolution.UrlParam: "http://foo", + gitresolution.RepoParam: "foo", + }, + expectedErr: "cannot specify both 'url' and 'repo'", + }, { + name: "no org with repo", + params: map[string]string{ + gitresolution.RevisionParam: "abcd1234", + gitresolution.PathParam: "/foo/bar", + gitresolution.RepoParam: "foo", + }, + expectedErr: "'org' is required when 'repo' is specified", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := &Resolver{} + err := resolver.Validate(context.Background(), &v1beta1.ResolutionRequestSpec{Params: toParams(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 TestGetResolutionTimeoutDefault(t *testing.T) { + resolver := Resolver{} + defaultTimeout := 30 * time.Minute + timeout := resolver.GetResolutionTimeout(context.Background(), defaultTimeout) + if timeout != defaultTimeout { + t.Fatalf("expected default timeout to be returned") + } +} + +func TestGetResolutionTimeoutCustom(t *testing.T) { + resolver := Resolver{} + defaultTimeout := 30 * time.Minute + configTimeout := 5 * time.Second + config := map[string]string{ + gitresolution.DefaultTimeoutKey: configTimeout.String(), + } + ctx := resolutionframework.InjectResolverConfigToContext(context.Background(), config) + timeout := resolver.GetResolutionTimeout(ctx, defaultTimeout) + if timeout != configTimeout { + t.Fatalf("expected timeout from config to be returned") + } +} + +func TestResolveNotEnabled(t *testing.T) { + resolver := Resolver{} + + var err error + + someParams := map[string]string{ + gitresolution.PathParam: "bar", + gitresolution.RevisionParam: "baz", + } + _, err = resolver.Resolve(resolverDisabledContext(), &v1beta1.ResolutionRequestSpec{Params: toParams(someParams)}) + if err == nil { + t.Fatalf("expected disabled err") + } + if d := cmp.Diff(disabledError, err.Error()); d != "" { + t.Errorf("unexpected error: %s", diff.PrintWantGot(d)) + } +} + +type params struct { + url string + revision string + pathInRepo string + org string + repo string + token string + tokenKey string + namespace string + serverURL string + scmType string +} + +func TestResolve(t *testing.T) { + // local repo set up for anonymous cloning + // ---- + commits := []commitForRepo{{ + Dir: "foo/", + Filename: "old", + Content: "old content in test branch", + Branch: "test-branch", + }, { + Dir: "foo/", + Filename: "new", + Content: "new content in test branch", + Branch: "test-branch", + }, { + Dir: "./", + Filename: "released", + Content: "released content in main branch and in tag v1", + Tag: "v1", + }} + + anonFakeRepoURL, commitSHAsInAnonRepo := createTestRepo(t, commits) + + // local repo set up for scm cloning + // ---- + withTemporaryGitConfig(t) + + testOrg := "test-org" + testRepo := "test-repo" + + refsDir := filepath.Join("testdata", "test-org", "test-repo", "refs") + mainPipelineYAML, err := os.ReadFile(filepath.Join(refsDir, "main", "pipelines", "example-pipeline.yaml")) + if err != nil { + t.Fatalf("couldn't read main pipeline: %v", err) + } + otherPipelineYAML, err := os.ReadFile(filepath.Join(refsDir, "other", "pipelines", "example-pipeline.yaml")) + if err != nil { + t.Fatalf("couldn't read other pipeline: %v", err) + } + + mainTaskYAML, err := os.ReadFile(filepath.Join(refsDir, "main", "tasks", "example-task.yaml")) + if err != nil { + t.Fatalf("couldn't read main task: %v", err) + } + + commitSHAsInSCMRepo := []string{"abc", "xyz"} + + scmFakeRepoURL := fmt.Sprintf("https://fake/%s/%s.git", testOrg, testRepo) + resolver := &Resolver{ + clientFunc: func(driver string, serverURL string, token string, opts ...factory.ClientOptionFunc) (*scm.Client, error) { + scmClient, scmData := fake.NewDefault() + + // repository service + scmData.Repositories = []*scm.Repository{{ + FullName: fmt.Sprintf("%s/%s", testOrg, testRepo), + Clone: scmFakeRepoURL, + }} + + // git service + scmData.Commits = map[string]*scm.Commit{ + "main": {Sha: commitSHAsInSCMRepo[0]}, + "other": {Sha: commitSHAsInSCMRepo[1]}, + } + return scmClient, nil + }, + } + + testCases := []struct { + name string + args *params + config map[string]string + apiToken string + expectedCommitSHA string + expectedStatus *v1beta1.ResolutionRequestStatus + expectedErr error + }{{ + name: "clone: default revision main", + args: ¶ms{ + pathInRepo: "./released", + url: anonFakeRepoURL, + }, + expectedCommitSHA: commitSHAsInAnonRepo[2], + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + }, { + name: "clone: revision is tag name", + args: ¶ms{ + revision: "v1", + pathInRepo: "./released", + url: anonFakeRepoURL, + }, + expectedCommitSHA: commitSHAsInAnonRepo[2], + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + }, { + name: "clone: revision is the full tag name i.e. refs/tags/v1", + args: ¶ms{ + revision: "refs/tags/v1", + pathInRepo: "./released", + url: anonFakeRepoURL, + }, + expectedCommitSHA: commitSHAsInAnonRepo[2], + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + }, { + name: "clone: revision is a branch name", + args: ¶ms{ + revision: "test-branch", + pathInRepo: "foo/new", + url: anonFakeRepoURL, + }, + expectedCommitSHA: commitSHAsInAnonRepo[1], + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("new content in test branch")), + }, { + name: "clone: revision is a specific commit sha", + args: ¶ms{ + revision: commitSHAsInAnonRepo[0], + pathInRepo: "foo/old", + url: anonFakeRepoURL, + }, + expectedCommitSHA: commitSHAsInAnonRepo[0], + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("old content in test branch")), + }, { + name: "clone: file does not exist", + args: ¶ms{ + pathInRepo: "foo/non-exist", + url: anonFakeRepoURL, + }, + expectedErr: createError(`error opening file "foo/non-exist": file does not exist`), + }, { + name: "clone: revision does not exist", + args: ¶ms{ + revision: "non-existent-revision", + pathInRepo: "foo/new", + url: anonFakeRepoURL, + }, + expectedErr: createError("revision error: reference not found"), + }, { + name: "api: successful task from params api information", + args: ¶ms{ + revision: "main", + pathInRepo: "tasks/example-task.yaml", + org: testOrg, + repo: testRepo, + token: "token-secret", + tokenKey: "token", + namespace: "foo", + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + }, + apiToken: "some-token", + expectedCommitSHA: commitSHAsInSCMRepo[0], + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), + }, { + name: "api: successful task", + args: ¶ms{ + revision: "main", + pathInRepo: "tasks/example-task.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedCommitSHA: commitSHAsInSCMRepo[0], + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), + }, { + name: "api: successful pipeline", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedCommitSHA: commitSHAsInSCMRepo[0], + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainPipelineYAML), + }, { + name: "api: successful pipeline with default revision", + args: ¶ms{ + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + gitresolution.DefaultRevisionKey: "other", + }, + apiToken: "some-token", + expectedCommitSHA: commitSHAsInSCMRepo[1], + expectedStatus: resolution.CreateResolutionRequestStatusWithData(otherPipelineYAML), + }, { + name: "api: successful override scm type and server URL from user params", + + args: ¶ms{ + revision: "main", + pathInRepo: "tasks/example-task.yaml", + org: testOrg, + repo: testRepo, + token: "token-secret", + tokenKey: "token", + namespace: "foo", + scmType: "fake", + serverURL: "fake", + }, + config: map[string]string{ + gitresolution.ServerURLKey: "notsofake", + gitresolution.SCMTypeKey: "definitivelynotafake", + }, + apiToken: "some-token", + expectedCommitSHA: commitSHAsInSCMRepo[0], + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), + }, { + name: "api: file does not exist", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/other-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: createError("couldn't fetch resource content: file testdata/test-org/test-repo/refs/main/pipelines/other-pipeline.yaml does not exist: stat testdata/test-org/test-repo/refs/main/pipelines/other-pipeline.yaml: no such file or directory"), + }, { + name: "api: token not found", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: createError("cannot get API token, secret token-secret not found in namespace " + system.Namespace()), + }, { + name: "api: token secret name not specified", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: createError("cannot get API token, required when specifying 'repo' param, 'api-token-secret-name' not specified in config"), + }, { + name: "api: token secret key not specified", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.ServerURLKey: "fake", + gitresolution.SCMTypeKey: "fake", + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: createError("cannot get API token, required when specifying 'repo' param, 'api-token-secret-key' not specified in config"), + }, { + name: "api: SCM type not specified", + args: ¶ms{ + revision: "main", + pathInRepo: "pipelines/example-pipeline.yaml", + org: testOrg, + repo: testRepo, + }, + config: map[string]string{ + gitresolution.APISecretNameKey: "token-secret", + gitresolution.APISecretKeyKey: "token", + gitresolution.APISecretNamespaceKey: system.Namespace(), + }, + apiToken: "some-token", + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: createError("missing or empty scm-type value in configmap"), + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, _ := ttesting.SetupFakeContext(t) + + cfg := tc.config + if cfg == nil { + cfg = make(map[string]string) + } + cfg[gitresolution.DefaultTimeoutKey] = "1m" + if cfg[gitresolution.DefaultRevisionKey] == "" { + cfg[gitresolution.DefaultRevisionKey] = plumbing.Master.Short() + } + + request := createRequest(tc.args) + + d := test.Data{ + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: ConfigMapName, + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: cfg, + }, { + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + Name: resolverconfig.GetFeatureFlagsConfigName(), + }, + Data: map[string]string{ + "enable-git-resolver": "true", + }, + }}, + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + } + + var expectedStatus *v1beta1.ResolutionRequestStatus + if tc.expectedStatus != nil { + expectedStatus = tc.expectedStatus.DeepCopy() + + if tc.expectedErr == nil { + // status.annotations + if expectedStatus.Annotations == nil { + expectedStatus.Annotations = make(map[string]string) + } + expectedStatus.Annotations[common.AnnotationKeyContentType] = "application/x-yaml" + expectedStatus.Annotations[gitresolution.AnnotationKeyRevision] = tc.expectedCommitSHA + expectedStatus.Annotations[gitresolution.AnnotationKeyPath] = tc.args.pathInRepo + + if tc.args.url != "" { + expectedStatus.Annotations[gitresolution.AnnotationKeyURL] = anonFakeRepoURL + } else { + expectedStatus.Annotations[gitresolution.AnnotationKeyOrg] = testOrg + expectedStatus.Annotations[gitresolution.AnnotationKeyRepo] = testRepo + expectedStatus.Annotations[gitresolution.AnnotationKeyURL] = scmFakeRepoURL + } + + // status.refSource + expectedStatus.RefSource = &pipelinev1.RefSource{ + URI: "git+" + expectedStatus.Annotations[gitresolution.AnnotationKeyURL], + Digest: map[string]string{ + "sha1": tc.expectedCommitSHA, + }, + EntryPoint: tc.args.pathInRepo, + } + expectedStatus.Source = expectedStatus.RefSource + } else { + expectedStatus.Status.Conditions[0].Message = tc.expectedErr.Error() + } + } + + frtesting.RunResolverReconcileTest(ctx, t, d, resolver, request, expectedStatus, tc.expectedErr, func(resolver framework.Resolver, testAssets test.Assets) { + var secretName, secretNameKey, secretNamespace string + if tc.config[gitresolution.APISecretNameKey] != "" && tc.config[gitresolution.APISecretNamespaceKey] != "" && tc.config[gitresolution.APISecretKeyKey] != "" && tc.apiToken != "" { + secretName, secretNameKey, secretNamespace = tc.config[gitresolution.APISecretNameKey], tc.config[gitresolution.APISecretKeyKey], tc.config[gitresolution.APISecretNamespaceKey] + } + if tc.args.token != "" && tc.args.namespace != "" && tc.args.tokenKey != "" { + secretName, secretNameKey, secretNamespace = tc.args.token, tc.args.tokenKey, tc.args.namespace + } + if secretName == "" || secretNameKey == "" || secretNamespace == "" { + return + } + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + secretNameKey: []byte(base64.StdEncoding.Strict().EncodeToString([]byte(tc.apiToken))), + }, + Type: corev1.SecretTypeOpaque, + } + if _, err := testAssets.Clients.Kube.CoreV1().Secrets(secretNamespace).Create(ctx, tokenSecret, metav1.CreateOptions{}); err != nil { + t.Fatalf("failed to create test token secret: %v", err) + } + }) + }) + } +} + +// createTestRepo is used to instantiate a local test repository with the desired commits. +func createTestRepo(t *testing.T, commits []commitForRepo) (string, []string) { + t.Helper() + commitSHAs := []string{} + + t.Helper() + tempDir := t.TempDir() + + repo, err := git.PlainInit(tempDir, false) + + worktree, err := repo.Worktree() + if err != nil { + t.Fatalf("getting test worktree: %v", err) + } + if worktree == nil { + t.Fatal("test worktree not created") + } + + startingHash := writeAndCommitToTestRepo(t, worktree, tempDir, "", "README", []byte("This is a test")) + + hashesByBranch := make(map[string][]string) + + // Iterate over the commits and add them. + for _, cmt := range commits { + branch := cmt.Branch + if branch == "" { + branch = plumbing.Master.Short() + } + + // If we're given a revision, check out that revision. + coOpts := &git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branch), + } + + if _, ok := hashesByBranch[branch]; !ok && branch != plumbing.Master.Short() { + coOpts.Hash = plumbing.NewHash(startingHash.String()) + coOpts.Create = true + } + + if err := worktree.Checkout(coOpts); err != nil { + t.Fatalf("couldn't do checkout of %s: %v", branch, err) + } + + hash := writeAndCommitToTestRepo(t, worktree, tempDir, cmt.Dir, cmt.Filename, []byte(cmt.Content)) + commitSHAs = append(commitSHAs, hash.String()) + + if _, ok := hashesByBranch[branch]; !ok { + hashesByBranch[branch] = []string{hash.String()} + } else { + hashesByBranch[branch] = append(hashesByBranch[branch], hash.String()) + } + + if cmt.Tag != "" { + _, err = repo.CreateTag(cmt.Tag, hash, &git.CreateTagOptions{ + Message: cmt.Tag, + Tagger: &object.Signature{ + Name: "Someone", + Email: "someone@example.com", + When: time.Now(), + }, + }) + } + if err != nil { + t.Fatalf("couldn't add tag for %s: %v", cmt.Tag, err) + } + } + + return tempDir, commitSHAs +} + +// commitForRepo provides the directory, filename, content and revision for a test commit. +type commitForRepo struct { + Dir string + Filename string + Content string + Branch string + Tag string +} + +func writeAndCommitToTestRepo(t *testing.T, worktree *git.Worktree, repoDir string, subPath string, filename string, content []byte) plumbing.Hash { + t.Helper() + + targetDir := repoDir + if subPath != "" { + targetDir = filepath.Join(targetDir, subPath) + fi, err := os.Stat(targetDir) + if os.IsNotExist(err) { + if err := os.MkdirAll(targetDir, 0o700); err != nil { + t.Fatalf("couldn't create directory %s in worktree: %v", targetDir, err) + } + } else if err != nil { + t.Fatalf("checking if directory %s in worktree exists: %v", targetDir, err) + } + if fi != nil && !fi.IsDir() { + t.Fatalf("%s already exists but is not a directory", targetDir) + } + } + + outfile := filepath.Join(targetDir, filename) + if err := os.WriteFile(outfile, content, 0o600); err != nil { + t.Fatalf("couldn't write content to file %s: %v", outfile, err) + } + + _, err := worktree.Add(filepath.Join(subPath, filename)) + if err != nil { + t.Fatalf("couldn't add file %s to git: %v", outfile, err) + } + + hash, err := worktree.Commit("adding file for test", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Someone", + Email: "someone@example.com", + When: time.Now(), + }, + }) + if err != nil { + t.Fatalf("couldn't perform commit for test: %v", err) + } + + return hash +} + +// withTemporaryGitConfig resets the .gitconfig for the duration of the test. +func withTemporaryGitConfig(t *testing.T) { + t.Helper() + gitConfigDir := t.TempDir() + key := "GIT_CONFIG_GLOBAL" + t.Setenv(key, filepath.Join(gitConfigDir, "config")) +} + +func createRequest(args *params) *v1beta1.ResolutionRequest { + rr := &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + common.LabelKeyResolverType: labelValueGitResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: gitresolution.PathParam, + Value: *pipelinev1.NewStructuredValues(args.pathInRepo), + }}, + }, + } + + if args.revision != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.RevisionParam, + Value: *pipelinev1.NewStructuredValues(args.revision), + }) + } + + if args.serverURL != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.ServerURLParam, + Value: *pipelinev1.NewStructuredValues(args.serverURL), + }) + } + if args.scmType != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.ScmTypeParam, + Value: *pipelinev1.NewStructuredValues(args.scmType), + }) + } + + if args.url != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.UrlParam, + Value: *pipelinev1.NewStructuredValues(args.url), + }) + } else { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.RepoParam, + Value: *pipelinev1.NewStructuredValues(args.repo), + }) + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.OrgParam, + Value: *pipelinev1.NewStructuredValues(args.org), + }) + if args.token != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.TokenParam, + Value: *pipelinev1.NewStructuredValues(args.token), + }) + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: gitresolution.TokenKeyParam, + Value: *pipelinev1.NewStructuredValues(args.tokenKey), + }) + } + } + + return rr +} + +func resolverDisabledContext() context.Context { + return frameworktesting.ContextWithGitResolverDisabled(context.Background()) +} + +func createError(msg string) error { + return &common.GetResourceError{ + ResolverName: gitResolverName, + Key: "foo/rr", + Original: errors.New(msg), + } +} + +func toParams(m map[string]string) []pipelinev1.Param { + var params []pipelinev1.Param + + for k, v := range m { + params = append(params, pipelinev1.Param{ + Name: k, + Value: *pipelinev1.NewStructuredValues(v), + }) + } + + return params +} diff --git a/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/pipelines/example-pipeline.yaml b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/pipelines/example-pipeline.yaml new file mode 100644 index 00000000000..cc697dd2e91 --- /dev/null +++ b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/pipelines/example-pipeline.yaml @@ -0,0 +1,10 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: example-pipeline +spec: + tasks: + - name: some-pipeline-task + taskRef: + kind: Task + name: some-task diff --git a/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/tasks/example-task.yaml b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/tasks/example-task.yaml new file mode 100644 index 00000000000..97ad418341e --- /dev/null +++ b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/main/tasks/example-task.yaml @@ -0,0 +1,9 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: example-task +spec: + steps: + - command: ['something'] + image: some-image + name: some-step diff --git a/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/other/pipelines/example-pipeline.yaml b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/other/pipelines/example-pipeline.yaml new file mode 100644 index 00000000000..cfec4bb5618 --- /dev/null +++ b/pkg/remoteresolution/resolver/git/testdata/test-org/test-repo/refs/other/pipelines/example-pipeline.yaml @@ -0,0 +1,10 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: example-pipeline +spec: + tasks: + - name: some-pipeline-task + taskRef: + kind: Task + name: some-other-task diff --git a/pkg/remoteresolution/resolver/http/resolver.go b/pkg/remoteresolution/resolver/http/resolver.go new file mode 100644 index 00000000000..3a0a5d48f0c --- /dev/null +++ b/pkg/remoteresolution/resolver/http/resolver.go @@ -0,0 +1,100 @@ +/* +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 http + +import ( + "context" + "errors" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/http" + "go.uber.org/zap" + "k8s.io/client-go/kubernetes" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/pkg/logging" +) + +const ( + // LabelValueHttpResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueHttpResolverType string = "http" + + disabledError = "cannot handle resolution request, enable-http-resolver feature flag not true" + + // httpResolverName The name of the resolver + httpResolverName = "Http" + + // configMapName is the http resolver's config map + configMapName = "http-resolver-config" + + // default Timeout value when fetching http resources in seconds + defaultHttpTimeoutValue = "1m" + + // default key in the HTTP password secret + defaultBasicAuthSecretKey = "password" +) + +var _ framework.Resolver = &Resolver{} + +// Resolver implements a framework.Resolver that can fetch files from an HTTP URL +type Resolver struct { + kubeClient kubernetes.Interface + logger *zap.SugaredLogger +} + +func (r *Resolver) Initialize(ctx context.Context) error { + r.kubeClient = kubeclient.Get(ctx) + r.logger = logging.FromContext(ctx) + return nil +} + +// GetName returns a string name to refer to this resolver by. +func (r *Resolver) GetName(context.Context) string { + return httpResolverName +} + +// GetConfigName returns the name of the http resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return configMapName +} + +// GetSelector returns a map of labels to match requests to this resolver. +func (r *Resolver) GetSelector(context.Context) map[string]string { + return map[string]string{ + common.LabelKeyResolverType: LabelValueHttpResolverType, + } +} + +// Validate ensures parameters from a request are as expected. +func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + return http.ValidateParams(ctx, req.Params) +} + +// Resolve uses the given params to resolve the requested file or resource. +func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) { + oParams := req.Params + if http.IsDisabled(ctx) { + return nil, errors.New(disabledError) + } + + params, err := http.PopulateDefaultParams(ctx, oParams) + if err != nil { + return nil, err + } + + return http.FetchHttpResource(ctx, params, r.kubeClient, r.logger) +} diff --git a/pkg/remoteresolution/resolver/http/resolver_test.go b/pkg/remoteresolution/resolver/http/resolver_test.go new file mode 100644 index 00000000000..f6d4634822e --- /dev/null +++ b/pkg/remoteresolution/resolver/http/resolver_test.go @@ -0,0 +1,503 @@ +/* +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 http + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + frtesting "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework/testing" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + frameworktesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" + httpresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/http" + "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/system" + _ "knative.dev/pkg/system/testing" +) + +type params struct { + url string + authUsername string + authSecret string + authSecretKey string + authSecretContent string +} + +const sampleTask = `--- +kind: Task +apiVersion: tekton.dev/v1 +metadata: + name: foo +spec: + steps: + - name: step1 + image: scratch` +const emptyStr = "empty" + +func TestGetSelector(t *testing.T) { + resolver := Resolver{} + sel := resolver.GetSelector(context.Background()) + if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != LabelValueHttpResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidate(t *testing.T) { + testCases := []struct { + name string + url string + expectedErr error + }{ + { + name: "valid/url", + url: "https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.4/git-clone.yaml", + }, { + name: "invalid/url", + url: "xttps:ufoo/bar/", + expectedErr: errors.New(`url xttps:ufoo/bar/ is not a valid http(s) url`), + }, { + name: "invalid/url empty", + url: "", + expectedErr: errors.New(`cannot parse url : parse "": empty url`), + }, { + name: "missing/url", + expectedErr: errors.New(`missing required http resolver params: url`), + url: "nourl", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := Resolver{} + params := map[string]string{} + if tc.url != "nourl" { + params[httpresolution.UrlParam] = tc.url + } + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(params), + } + err := resolver.Validate(contextWithConfig(defaultHttpTimeoutValue), &req) + if tc.expectedErr != nil { + checkExpectedErr(t, tc.expectedErr, err) + } else if err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + }) + } +} + +func TestResolve(t *testing.T) { + tests := []struct { + name string + expectedErr string + input string + paramSet bool + expectedStatus int + }{ + { + name: "good/params set", + input: "task", + paramSet: true, + }, { + name: "bad/params not set", + input: "task", + expectedErr: `missing required http resolver params: url`, + }, { + name: "bad/not found", + input: "task", + paramSet: true, + expectedStatus: http.StatusNotFound, + expectedErr: `requested URL 'http://([^']*)' is not found`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tc.expectedStatus != 0 { + w.WriteHeader(tc.expectedStatus) + } + fmt.Fprintf(w, tc.input) + })) + params := []pipelinev1.Param{} + if tc.paramSet { + params = append(params, pipelinev1.Param{ + Name: httpresolution.UrlParam, + Value: *pipelinev1.NewStructuredValues(svr.URL), + }) + } + resolver := Resolver{} + req := v1beta1.ResolutionRequestSpec{ + Params: params, + } + output, err := resolver.Resolve(contextWithConfig(defaultHttpTimeoutValue), &req) + if tc.expectedErr != "" { + re := regexp.MustCompile(tc.expectedErr) + if !re.MatchString(err.Error()) { + t.Fatalf("expected error '%v' but got '%v'", tc.expectedErr, err) + } + return + } else if err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + if o := cmp.Diff(tc.input, string(output.Data())); o != "" { + t.Fatalf("expected output '%v' but got '%v'", tc.input, string(output.Data())) + } + if o := cmp.Diff(svr.URL, output.RefSource().URI); o != "" { + t.Fatalf("expected url '%v' but got '%v'", svr.URL, output.RefSource().URI) + } + + eSum := sha256.New() + eSum.Write([]byte(tc.input)) + eSha256 := hex.EncodeToString(eSum.Sum(nil)) + if o := cmp.Diff(eSha256, output.RefSource().Digest["sha256"]); o != "" { + t.Fatalf("expected sha256 '%v' but got '%v'", eSha256, output.RefSource().Digest["sha256"]) + } + + if output.Annotations() != nil { + t.Fatalf("output annotations should be nil") + } + }) + } +} + +func TestResolveNotEnabled(t *testing.T) { + var err error + resolver := Resolver{} + someParams := map[string]string{} + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(someParams), + } + _, err = resolver.Resolve(resolverDisabledContext(), &req) + if err == nil { + t.Fatalf("expected disabled err") + } + if d := cmp.Diff(disabledError, err.Error()); d != "" { + t.Errorf("unexpected error: %s", diff.PrintWantGot(d)) + } + err = resolver.Validate(resolverDisabledContext(), &v1beta1.ResolutionRequestSpec{Params: toParams(map[string]string{})}) + 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 TestResolverReconcileBasicAuth(t *testing.T) { + var doNotCreate string = "notcreate" + var wrongSecretKey string = "wrongsecretk" + + tests := []struct { + name string + params *params + taskContent string + expectedStatus *v1beta1.ResolutionRequestStatus + expectedErr error + }{ + { + name: "good/URL Resolution", + taskContent: sampleTask, + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + }, + { + name: "good/URL Resolution with custom basic auth, and custom secret key", + taskContent: sampleTask, + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + params: ¶ms{ + authSecret: "auth-secret", + authUsername: "auth", + authSecretKey: "token", + authSecretContent: "untoken", + }, + }, + { + name: "good/URL Resolution with custom basic auth no custom secret key", + taskContent: sampleTask, + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + params: ¶ms{ + authSecret: "auth-secret", + authUsername: "auth", + authSecretContent: "untoken", + }, + }, + { + name: "bad/no url found", + params: ¶ms{}, + expectedErr: errors.New(`invalid resource request "foo/rr": cannot parse url : parse "": empty url`), + }, + { + name: "bad/no secret found", + params: ¶ms{ + authSecret: doNotCreate, + authUsername: "user", + url: "https://blah/blah.com", + }, + expectedErr: errors.New(`error getting "Http" "foo/rr": cannot get API token, secret notcreate not found in namespace foo`), + }, + { + name: "bad/no valid secret key", + params: ¶ms{ + authSecret: "shhhhh", + authUsername: "user", + authSecretKey: wrongSecretKey, + url: "https://blah/blah", + }, + expectedErr: errors.New(`error getting "Http" "foo/rr": cannot get API token, key wrongsecretk not found in secret shhhhh in namespace foo`), + }, + { + name: "bad/missing username params for secret with params", + params: ¶ms{ + authSecret: "shhhhh", + url: "https://blah/blah", + }, + expectedErr: errors.New(`invalid resource request "foo/rr": missing required param http-username when using http-password-secret`), + }, + { + name: "bad/missing password params for secret with username", + params: ¶ms{ + authUsername: "failure", + url: "https://blah/blah", + }, + expectedErr: errors.New(`invalid resource request "foo/rr": missing required param http-password-secret when using http-username`), + }, + { + name: "bad/empty auth username", + params: ¶ms{ + authUsername: emptyStr, + authSecret: "asecret", + url: "https://blah/blah", + }, + expectedErr: errors.New(`invalid resource request "foo/rr": value http-username cannot be empty`), + }, + { + name: "bad/empty auth password", + params: ¶ms{ + authUsername: "auser", + authSecret: emptyStr, + url: "https://blah/blah", + }, + expectedErr: errors.New(`invalid resource request "foo/rr": value http-password-secret cannot be empty`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := &Resolver{} + ctx, _ := ttesting.SetupFakeContext(t) + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, tt.taskContent) + })) + p := tt.params + if p == nil { + p = ¶ms{} + } + if p.url == "" && tt.taskContent != "" { + p.url = svr.URL + } + request := createRequest(p) + cfg := make(map[string]string) + d := test.Data{ + ConfigMaps: []*corev1.ConfigMap{{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + }, + Data: cfg, + }, { + ObjectMeta: metav1.ObjectMeta{ + Namespace: resolverconfig.ResolversNamespace(system.Namespace()), + Name: resolverconfig.GetFeatureFlagsConfigName(), + }, + Data: map[string]string{ + "enable-http-resolver": "true", + }, + }}, + ResolutionRequests: []*v1beta1.ResolutionRequest{request}, + } + var expectedStatus *v1beta1.ResolutionRequestStatus + if tt.expectedStatus != nil { + expectedStatus = tt.expectedStatus.DeepCopy() + if tt.expectedErr == nil { + if tt.taskContent != "" { + h := sha256.New() + h.Write([]byte(tt.taskContent)) + sha256CheckSum := hex.EncodeToString(h.Sum(nil)) + refsrc := &pipelinev1.RefSource{ + URI: svr.URL, + Digest: map[string]string{ + "sha256": sha256CheckSum, + }, + } + expectedStatus.RefSource = refsrc + expectedStatus.Source = refsrc + } + } else { + expectedStatus.Status.Conditions[0].Message = tt.expectedErr.Error() + } + } + frtesting.RunResolverReconcileTest(ctx, t, d, resolver, request, expectedStatus, tt.expectedErr, func(resolver framework.Resolver, testAssets test.Assets) { + if err := resolver.Initialize(ctx); err != nil { + t.Errorf("unexpected error: %v", err) + } + if tt.params == nil { + return + } + if tt.params.authSecret != "" && tt.params.authSecret != doNotCreate { + secretKey := tt.params.authSecretKey + if secretKey == wrongSecretKey { + secretKey = "randomNotOund" + } + if secretKey == "" { + secretKey = defaultBasicAuthSecretKey + } + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: tt.params.authSecret, + Namespace: request.GetNamespace(), + }, + Data: map[string][]byte{ + secretKey: []byte(base64.StdEncoding.Strict().EncodeToString([]byte(tt.params.authSecretContent))), + }, + } + if _, err := testAssets.Clients.Kube.CoreV1().Secrets(request.GetNamespace()).Create(ctx, tokenSecret, metav1.CreateOptions{}); err != nil { + t.Fatalf("failed to create test token secret: %v", err) + } + } + }) + }) + } +} + +func TestGetName(t *testing.T) { + resolver := Resolver{} + ctx := context.Background() + + if d := cmp.Diff(httpResolverName, resolver.GetName(ctx)); d != "" { + t.Errorf("invalid name: %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(configMapName, resolver.GetConfigName(ctx)); d != "" { + t.Errorf("invalid config map name: %s", diff.PrintWantGot(d)) + } +} + +func resolverDisabledContext() context.Context { + return frameworktesting.ContextWithHttpResolverDisabled(context.Background()) +} + +func toParams(m map[string]string) []pipelinev1.Param { + var params []pipelinev1.Param + + for k, v := range m { + params = append(params, pipelinev1.Param{ + Name: k, + Value: *pipelinev1.NewStructuredValues(v), + }) + } + + return params +} + +func contextWithConfig(timeout string) context.Context { + config := map[string]string{ + httpresolution.TimeoutKey: timeout, + } + return resolutionframework.InjectResolverConfigToContext(context.Background(), config) +} + +func checkExpectedErr(t *testing.T, expectedErr, actualErr error) { + t.Helper() + if actualErr == nil { + t.Fatalf("expected err '%v' but didn't get one", expectedErr) + } + if d := cmp.Diff(expectedErr.Error(), actualErr.Error()); d != "" { + t.Fatalf("expected err '%v' but got '%v'", expectedErr, actualErr) + } +} + +func createRequest(params *params) *v1beta1.ResolutionRequest { + rr := &v1beta1.ResolutionRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "resolution.tekton.dev/v1beta1", + Kind: "ResolutionRequest", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rr", + Namespace: "foo", + CreationTimestamp: metav1.Time{Time: time.Now()}, + Labels: map[string]string{ + resolutioncommon.LabelKeyResolverType: LabelValueHttpResolverType, + }, + }, + Spec: v1beta1.ResolutionRequestSpec{ + Params: []pipelinev1.Param{{ + Name: httpresolution.UrlParam, + Value: *pipelinev1.NewStructuredValues(params.url), + }}, + }, + } + if params.authSecret != "" { + s := params.authSecret + if s == emptyStr { + s = "" + } + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: httpresolution.HttpBasicAuthSecret, + Value: *pipelinev1.NewStructuredValues(s), + }) + } + + if params.authUsername != "" { + s := params.authUsername + if s == emptyStr { + s = "" + } + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: httpresolution.HttpBasicAuthUsername, + Value: *pipelinev1.NewStructuredValues(s), + }) + } + + if params.authSecretKey != "" { + rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ + Name: httpresolution.HttpBasicAuthSecretKey, + Value: *pipelinev1.NewStructuredValues(params.authSecretKey), + }) + } + + return rr +} diff --git a/pkg/remoteresolution/resolver/hub/resolver.go b/pkg/remoteresolution/resolver/hub/resolver.go new file mode 100644 index 00000000000..fbea8b32709 --- /dev/null +++ b/pkg/remoteresolution/resolver/hub/resolver.go @@ -0,0 +1,78 @@ +/* +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 hub + +import ( + "context" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + "github.com/tektoncd/pipeline/pkg/resolution/resolver/hub" +) + +const ( + // LabelValueHubResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueHubResolverType string = "hub" + + // ArtifactHubType is the value to use setting the type field to artifact + ArtifactHubType string = "artifact" + + // TektonHubType is the value to use setting the type field to tekton + TektonHubType string = "tekton" +) + +var _ framework.Resolver = &Resolver{} + +// Resolver implements a framework.Resolver that can fetch files from OCI bundles. +type Resolver struct { + // TektonHubURL is the URL for hub resolver with type tekton + TektonHubURL string + // ArtifactHubURL is the URL for hub resolver with type artifact + ArtifactHubURL string +} + +// Initialize sets up any dependencies needed by the resolver. None atm. +func (r *Resolver) Initialize(context.Context) error { + return nil +} + +// GetName returns a string name to refer to this resolver by. +func (r *Resolver) GetName(context.Context) string { + return "Hub" +} + +// GetConfigName returns the name of the bundle resolver's configmap. +func (r *Resolver) GetConfigName(context.Context) string { + return "hubresolver-config" +} + +// GetSelector returns a map of labels to match requests to this resolver. +func (r *Resolver) GetSelector(context.Context) map[string]string { + return map[string]string{ + common.LabelKeyResolverType: LabelValueHubResolverType, + } +} + +// Validate ensures parameters from a request are as expected. +func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { + return hub.ValidateParams(ctx, req.Params, r.TektonHubURL) +} + +// Resolve uses the given params to resolve the requested file or resource. +func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) { + return hub.Resolve(ctx, req.Params, r.TektonHubURL, r.ArtifactHubURL) +} diff --git a/pkg/remoteresolution/resolver/hub/resolver_test.go b/pkg/remoteresolution/resolver/hub/resolver_test.go new file mode 100644 index 00000000000..bb5f7d19bff --- /dev/null +++ b/pkg/remoteresolution/resolver/hub/resolver_test.go @@ -0,0 +1,312 @@ +/* +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 hub + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionframework "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" + hubresolver "github.com/tektoncd/pipeline/pkg/resolution/resolver/hub" + "github.com/tektoncd/pipeline/test/diff" +) + +func TestGetSelector(t *testing.T) { + resolver := Resolver{} + sel := resolver.GetSelector(context.Background()) + if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + t.Fatalf("unexpected selector: %v", sel) + } else if typ != LabelValueHubResolverType { + t.Fatalf("unexpected type: %q", typ) + } +} + +func TestValidate(t *testing.T) { + testCases := []struct { + testName string + kind string + version string + catalog string + resourceName string + hubType string + expectedErr error + }{ + { + testName: "artifact type validation", + kind: "task", + resourceName: "foo", + version: "bar", + catalog: "baz", + hubType: ArtifactHubType, + }, { + testName: "tekton type validation", + kind: "task", + resourceName: "foo", + version: "bar", + catalog: "baz", + hubType: TektonHubType, + expectedErr: errors.New("failed to validate params: please configure TEKTON_HUB_API env variable to use tekton type"), + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + resolver := Resolver{} + params := map[string]string{ + hubresolver.ParamKind: tc.kind, + hubresolver.ParamName: tc.resourceName, + hubresolver.ParamVersion: tc.version, + hubresolver.ParamCatalog: tc.catalog, + hubresolver.ParamType: tc.hubType, + } + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(params), + } + err := resolver.Validate(contextWithConfig(), &req) + if tc.expectedErr != nil { + checkExpectedErr(t, tc.expectedErr, err) + } else if err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + }) + } +} +func TestValidateMissing(t *testing.T) { + resolver := Resolver{} + + var err error + + paramsMissingName := map[string]string{ + hubresolver.ParamKind: "foo", + hubresolver.ParamVersion: "bar", + } + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(paramsMissingName), + } + err = resolver.Validate(contextWithConfig(), &req) + if err == nil { + t.Fatalf("expected missing name err") + } + + paramsMissingVersion := map[string]string{ + hubresolver.ParamKind: "foo", + hubresolver.ParamName: "bar", + } + req = v1beta1.ResolutionRequestSpec{ + Params: toParams(paramsMissingVersion), + } + err = resolver.Validate(contextWithConfig(), &req) + + if err == nil { + t.Fatalf("expected missing version err") + } +} + +func TestValidateConflictingKindName(t *testing.T) { + testCases := []struct { + kind string + name string + version string + catalog string + hubType string + }{ + { + kind: "not-taskpipeline", + name: "foo", + version: "bar", + catalog: "baz", + hubType: TektonHubType, + }, + { + kind: "task", + name: "foo", + version: "bar", + catalog: "baz", + hubType: "not-tekton-artifact", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := Resolver{} + params := map[string]string{ + hubresolver.ParamKind: tc.kind, + hubresolver.ParamName: tc.name, + hubresolver.ParamVersion: tc.version, + hubresolver.ParamCatalog: tc.catalog, + hubresolver.ParamType: tc.hubType, + } + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(params), + } + err := resolver.Validate(contextWithConfig(), &req) + if err == nil { + t.Fatalf("expected err due to conflicting param") + } + }) + } +} + +func TestResolve(t *testing.T) { + testCases := []struct { + name string + kind string + imageName string + version string + catalog string + hubType string + input string + expectedRes []byte + expectedErr error + }{ + { + name: "valid response from Tekton Hub", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: TektonHubType, + input: `{"data":{"yaml":"some content"}}`, + expectedRes: []byte("some content"), + }, + { + name: "valid response from Artifact Hub", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: ArtifactHubType, + input: `{"data":{"manifestRaw":"some content"}}`, + expectedRes: []byte("some content"), + }, + { + name: "not-found response from hub", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: TektonHubType, + input: `{"name":"not-found","id":"aaaaaaaa","message":"resource not found","temporary":false,"timeout":false,"fault":false}`, + expectedRes: []byte(""), + }, + { + name: "response with bad formatting error", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: TektonHubType, + input: `value`, + expectedErr: errors.New("fail to fetch Tekton Hub resource: error unmarshalling json response: invalid character 'v' looking for beginning of value"), + }, + { + name: "response with empty body error from Tekton Hub", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: TektonHubType, + expectedErr: errors.New("fail to fetch Tekton Hub resource: error unmarshalling json response: unexpected end of JSON input"), + }, + { + name: "response with empty body error from Artifact Hub", + kind: "task", + imageName: "foo", + version: "baz", + catalog: "Tekton", + hubType: ArtifactHubType, + expectedErr: errors.New("fail to fetch Artifact Hub resource: error unmarshalling json response: unexpected end of JSON input"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, tc.input) + })) + + resolver := &Resolver{ + TektonHubURL: svr.URL, + ArtifactHubURL: svr.URL, + } + + params := map[string]string{ + hubresolver.ParamKind: tc.kind, + hubresolver.ParamName: tc.imageName, + hubresolver.ParamVersion: tc.version, + hubresolver.ParamCatalog: tc.catalog, + hubresolver.ParamType: tc.hubType, + } + req := v1beta1.ResolutionRequestSpec{ + Params: toParams(params), + } + output, err := resolver.Resolve(contextWithConfig(), &req) + if tc.expectedErr != nil { + checkExpectedErr(t, tc.expectedErr, err) + } else { + if err != nil { + t.Fatalf("unexpected error resolving: %v", err) + } + if d := cmp.Diff(tc.expectedRes, output.Data()); d != "" { + t.Errorf("unexpected resource from Resolve: %s", diff.PrintWantGot(d)) + } + } + }) + } +} + +func toParams(m map[string]string) []pipelinev1.Param { + var params []pipelinev1.Param + + for k, v := range m { + params = append(params, pipelinev1.Param{ + Name: k, + Value: *pipelinev1.NewStructuredValues(v), + }) + } + + return params +} + +func contextWithConfig() context.Context { + config := map[string]string{ + "default-tekton-hub-catalog": "Tekton", + "default-artifact-hub-task-catalog": "tekton-catalog-tasks", + "default-artifact-hub-pipeline-catalog": "tekton-catalog-pipelines", + "default-type": "artifact", + } + + return resolutionframework.InjectResolverConfigToContext(context.Background(), config) +} + +func checkExpectedErr(t *testing.T, expectedErr, actualErr error) { + t.Helper() + if actualErr == nil { + t.Fatalf("expected err '%v' but didn't get one", expectedErr) + } + if d := cmp.Diff(expectedErr.Error(), actualErr.Error()); d != "" { + t.Fatalf("expected err '%v' but got '%v'", expectedErr, actualErr) + } +} diff --git a/pkg/remoteresolution/resource/crd_resource.go b/pkg/remoteresolution/resource/crd_resource.go new file mode 100644 index 00000000000..017654ca005 --- /dev/null +++ b/pkg/remoteresolution/resource/crd_resource.go @@ -0,0 +1,92 @@ +/* +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 resource + +import ( + "context" + "errors" + + rrclient "github.com/tektoncd/pipeline/pkg/client/resolution/clientset/versioned" + rrlisters "github.com/tektoncd/pipeline/pkg/client/resolution/listers/resolution/v1beta1" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + resolutionresource "github.com/tektoncd/pipeline/pkg/resolution/resource" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" +) + +// CRDRequester implements the Requester interface using +// ResolutionRequest CRDs. +type CRDRequester struct { + clientset rrclient.Interface + lister rrlisters.ResolutionRequestLister +} + +// NewCRDRequester returns an implementation of Requester that uses +// ResolutionRequest CRD objects to mediate between the caller who wants a +// resource (e.g. Tekton Pipelines) and the responder who can fetch +// it (e.g. the gitresolver) +func NewCRDRequester(clientset rrclient.Interface, lister rrlisters.ResolutionRequestLister) *CRDRequester { + return &CRDRequester{clientset, lister} +} + +var _ Requester = &CRDRequester{} + +// Submit constructs a ResolutionRequest object and submits it to the +// kubernetes cluster, returning any errors experienced while doing so. +// If ResolutionRequest is succeeded then it returns the resolved data. +func (r *CRDRequester) Submit(ctx context.Context, resolver ResolverName, req Request) (ResolvedResource, error) { + rr, _ := r.lister.ResolutionRequests(req.ResolverPayload().Namespace).Get(req.ResolverPayload().Name) + if rr == nil { + if err := r.createResolutionRequest(ctx, resolver, req); err != nil && + // When the request reconciles frequently, the creation may fail + // because the list informer cache is not updated. + // If the request already exists then we can assume that is in progress. + // The next reconcile will handle it based on the actual situation. + !apierrors.IsAlreadyExists(err) { + return nil, err + } + return nil, resolutioncommon.ErrRequestInProgress + } + + if rr.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() { + // TODO(sbwsg): This should be where an existing + // resource is given an additional owner reference so + // that it doesn't get deleted until the caller is done + // with it. Use appendOwnerReference and then submit + // update to ResolutionRequest. + return nil, resolutioncommon.ErrRequestInProgress + } + + if rr.Status.GetCondition(apis.ConditionSucceeded).IsTrue() { + return resolutionresource.CrdIntoResource(rr), nil + } + + message := rr.Status.GetCondition(apis.ConditionSucceeded).GetMessage() + err := resolutioncommon.NewError(resolutioncommon.ReasonResolutionFailed, errors.New(message)) + return nil, err +} + +func (r *CRDRequester) createResolutionRequest(ctx context.Context, resolver ResolverName, req Request) error { + var owner metav1.OwnerReference + if ownedReq, ok := req.(OwnedRequest); ok { + owner = ownedReq.OwnerRef() + } + rr := resolutionresource.CreateResolutionRequest(ctx, resolver, req.ResolverPayload().Name, req.ResolverPayload().Namespace, req.ResolverPayload().ResolutionSpec.Params, owner) + _, err := r.clientset.ResolutionV1beta1().ResolutionRequests(rr.Namespace).Create(ctx, rr, metav1.CreateOptions{}) + return err +} diff --git a/pkg/remoteresolution/resource/crd_resource_test.go b/pkg/remoteresolution/resource/crd_resource_test.go new file mode 100644 index 00000000000..6bc4150e67e --- /dev/null +++ b/pkg/remoteresolution/resource/crd_resource_test.go @@ -0,0 +1,324 @@ +/* +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 resource_test + +import ( + "context" + "encoding/base64" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/test" + "github.com/tektoncd/pipeline/test/diff" + resolution "github.com/tektoncd/pipeline/test/remoteresolution" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/logging" + _ "knative.dev/pkg/system/testing" // Setup system.Namespace() + "sigs.k8s.io/yaml" +) + +// getCRDRequester returns an instance of the CRDRequester that has been seeded with +// d, where d represents the state of the system (existing resources) needed for the test. +func getCRDRequester(t *testing.T, d test.Data) (test.Assets, func()) { + t.Helper() + return initializeCRDRequesterAssets(t, d) +} + +func initializeCRDRequesterAssets(t *testing.T, d test.Data) (test.Assets, func()) { + t.Helper() + ctx, _ := ttesting.SetupFakeContext(t) + ctx, cancel := context.WithCancel(ctx) + c, informers := test.SeedTestData(t, ctx, d) + + return test.Assets{ + Logger: logging.FromContext(ctx), + Clients: c, + Informers: informers, + Ctx: ctx, + }, cancel +} + +func TestCRDRequesterSubmit(t *testing.T) { + ownerRef := mustParseOwnerReference(t, ` +apiVersion: tekton.dev/v1beta1 +blockOwnerDeletion: true +controller: true +kind: TaskRun +name: git-clone +uid: 727019c3-4066-4d8b-919e-90660dfd8b55 +`) + request := mustParseRawRequest(t, ` +resolverPayload: + name: git-ec247f5592afcaefa8485e34d2bd80c6 + namespace: namespace + resolutionSpec: + params: + - name: url + value: https://github.com/tektoncd/catalog + - name: revision + value: main + - name: pathInRepo + value: task/git-clone/0.6/git-clone.yaml +`) + baseRR := mustParseResolutionRequest(t, ` +kind: "ResolutionRequest" +apiVersion: "resolution.tekton.dev/v1beta1" +metadata: + name: "git-ec247f5592afcaefa8485e34d2bd80c6" + namespace: "namespace" + labels: + resolution.tekton.dev/type: "git" + ownerReferences: + - apiVersion: tekton.dev/v1beta1 + blockOwnerDeletion: true + controller: true + kind: TaskRun + name: git-clone + uid: 727019c3-4066-4d8b-919e-90660dfd8b55 +spec: + params: + - name: "url" + value: "https://github.com/tektoncd/catalog" + - name: "revision" + value: "main" + - name: "pathInRepo" + value: "task/git-clone/0.6/git-clone.yaml" +`) + createdRR := baseRR.DeepCopy() + // + unknownRR := baseRR.DeepCopy() + unknownRR.Status = *mustParseResolutionRequestStatus(t, ` +conditions: + - lastTransitionTime: "2023-03-26T10:31:29Z" + status: "Unknown" + type: Succeeded +`) + // + failedRR := baseRR.DeepCopy() + failedRR.Status = *mustParseResolutionRequestStatus(t, ` +conditions: + - lastTransitionTime: "2023-03-26T10:31:29Z" + status: "Failed" + type: Succeeded + message: "error message" +`) + // + successRR := baseRR.DeepCopy() + successRR.Status = *mustParseResolutionRequestStatus(t, ` +annotations: + resolution.tekton.dev/content-type: application/x-yaml + resolution.tekton.dev/path: task/git-clone/0.6/git-clone.yaml + resolution.tekton.dev/revision: main + resolution.tekton.dev/url: https://github.com/tektoncd/catalog +conditions: + - lastTransitionTime: "2023-03-26T10:31:29Z" + status: "True" + type: Succeeded + data: e30= +`) + // + successWithoutAnnotationsRR := baseRR.DeepCopy() + successWithoutAnnotationsRR.Status = *mustParseResolutionRequestStatus(t, ` +conditions: + - lastTransitionTime: "2023-03-26T10:31:29Z" + status: "True" + type: Succeeded + data: e30= +`) + + testCases := []struct { + name string + inputRequest *resolution.RawRequest + inputResolutionRequest *v1beta1.ResolutionRequest + expectedResolutionRequest *v1beta1.ResolutionRequest + expectedResolvedResource *v1beta1.ResolutionRequest + expectedErr error + }{ + { + name: "resolution request does not exist and needs to be created", + inputRequest: request, + inputResolutionRequest: nil, + expectedResolutionRequest: createdRR.DeepCopy(), + expectedResolvedResource: nil, + expectedErr: resolutioncommon.ErrRequestInProgress, + }, + { + name: "resolution request exist and status is unknown", + inputRequest: request, + inputResolutionRequest: unknownRR.DeepCopy(), + expectedResolutionRequest: nil, + expectedResolvedResource: nil, + expectedErr: resolutioncommon.ErrRequestInProgress, + }, + { + name: "resolution request exist and status is succeeded", + inputRequest: request, + inputResolutionRequest: successRR.DeepCopy(), + expectedResolutionRequest: nil, + expectedResolvedResource: successRR.DeepCopy(), + expectedErr: nil, + }, + { + name: "resolution request exist and status is succeeded but annotations is nil", + inputRequest: request, + inputResolutionRequest: successWithoutAnnotationsRR.DeepCopy(), + expectedResolutionRequest: nil, + expectedResolvedResource: successWithoutAnnotationsRR.DeepCopy(), + expectedErr: nil, + }, + { + name: "resolution request exist and status is failed", + inputRequest: request, + inputResolutionRequest: failedRR.DeepCopy(), + expectedResolutionRequest: nil, + expectedResolvedResource: nil, + expectedErr: resolutioncommon.NewError(resolutioncommon.ReasonResolutionFailed, errors.New("error message")), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := test.Data{} + if tc.inputResolutionRequest != nil { + d.ResolutionRequests = []*v1beta1.ResolutionRequest{tc.inputResolutionRequest} + } + + testAssets, cancel := getCRDRequester(t, d) + defer cancel() + ctx := testAssets.Ctx + clients := testAssets.Clients + + resolver := resolutioncommon.ResolverName("git") + crdRequester := resource.NewCRDRequester(clients.ResolutionRequests, testAssets.Informers.ResolutionRequest.Lister()) + requestWithOwner := &ownerRequest{ + Request: tc.inputRequest.Request(), + ownerRef: *ownerRef, + } + resolvedResource, err := crdRequester.Submit(ctx, resolver, requestWithOwner) + + // check the error + if err != nil || tc.expectedErr != nil { + if err == nil || tc.expectedErr == nil { + t.Errorf("expected error %v, but got %v", tc.expectedErr, err) + } else if err.Error() != tc.expectedErr.Error() { + t.Errorf("expected error %v, but got %v", tc.expectedErr, err) + } + } + + // check the resolved resource + switch { + case tc.expectedResolvedResource == nil: + // skipping check of resolved resources. + case tc.expectedResolvedResource != nil: + if resolvedResource == nil { + t.Errorf("expected resolved resource equal %v, but got %v", tc.expectedResolvedResource, resolvedResource) + break + } + rr := tc.expectedResolvedResource + data, err := base64.StdEncoding.Strict().DecodeString(rr.Status.Data) + if err != nil { + t.Errorf("unexpected error decoding expected resource data: %v", err) + } + expectedResolvedResource := resolution.NewResolvedResource(data, rr.Status.Annotations, rr.Status.RefSource, nil) + assertResolvedResourceEqual(t, expectedResolvedResource, resolvedResource) + } + + // check the resolution request + if tc.expectedResolutionRequest != nil { + resolutionrequest, err := clients.ResolutionRequests.ResolutionV1beta1(). + ResolutionRequests(tc.inputRequest.ResolverPayload.Namespace).Get(ctx, tc.inputRequest.ResolverPayload.Name, metav1.GetOptions{}) + if err != nil { + t.Errorf("unexpected error getting resource requests: %v", err) + } + if d := cmp.Diff(tc.expectedResolutionRequest, resolutionrequest); d != "" { + t.Errorf("expected resolution request to match %s", diff.PrintWantGot(d)) + } + } + }) + } +} + +type ownerRequest struct { + resource.Request + ownerRef metav1.OwnerReference +} + +func (r *ownerRequest) OwnerRef() metav1.OwnerReference { + return r.ownerRef +} + +func mustParseRawRequest(t *testing.T, yamlStr string) *resolution.RawRequest { + t.Helper() + output := &resolution.RawRequest{} + if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil { + t.Errorf("parsing raw request %s: %v", yamlStr, err) + } + return output +} + +func mustParseOwnerReference(t *testing.T, yamlStr string) *metav1.OwnerReference { + t.Helper() + output := &metav1.OwnerReference{} + if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil { + t.Errorf("parsing owner reference %s: %v", yamlStr, err) + } + return output +} + +func mustParseResolutionRequest(t *testing.T, yamlStr string) *v1beta1.ResolutionRequest { + t.Helper() + output := &v1beta1.ResolutionRequest{} + if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil { + t.Errorf("parsing resolution request %s: %v", yamlStr, err) + } + return output +} + +func mustParseResolutionRequestStatus(t *testing.T, yamlStr string) *v1beta1.ResolutionRequestStatus { + t.Helper() + output := &v1beta1.ResolutionRequestStatus{} + if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil { + t.Errorf("parsing resolution request status %s: %v", yamlStr, err) + } + return output +} + +func assertResolvedResourceEqual(t *testing.T, expected, actual resolutioncommon.ResolvedResource) { + t.Helper() + expectedBytes, err := expected.Data() + if err != nil { + t.Errorf("unexpected error getting expected resource data: %v", err) + } + actualBytes, err := actual.Data() + if err != nil { + t.Errorf("unexpected error getting acutal resource data: %v", err) + } + if d := cmp.Diff(expectedBytes, actualBytes); d != "" { + t.Errorf("expected resolved resource Data to match %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(expected.Annotations(), actual.Annotations()); d != "" { + t.Errorf("expected resolved resource Annotations to match %s", diff.PrintWantGot(d)) + } + if d := cmp.Diff(expected.RefSource(), actual.RefSource()); d != "" { + t.Errorf("expected resolved resource Source to match %s", diff.PrintWantGot(d)) + } +} diff --git a/pkg/remoteresolution/resource/doc.go b/pkg/remoteresolution/resource/doc.go new file mode 100644 index 00000000000..a78d6b68855 --- /dev/null +++ b/pkg/remoteresolution/resource/doc.go @@ -0,0 +1,24 @@ +/* +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 resource contains the upgraded remote resolution framework. +It is equivalent to `pkg/resolution/resource`. +This was necessary to ensure backwards compatibility with the existing framework. + +This framework is `ALPHA` and subject to further refactoring and changes. +*/ +package resource diff --git a/pkg/remoteresolution/resource/request.go b/pkg/remoteresolution/resource/request.go new file mode 100644 index 00000000000..f5472b472fd --- /dev/null +++ b/pkg/remoteresolution/resource/request.go @@ -0,0 +1,83 @@ +/* +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 resource + +import ( + "context" + + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" +) + +type BasicRequest struct { + resolverPayload ResolverPayload +} + +var _ Request = &BasicRequest{} + +// NewRequest returns an instance of a BasicRequestV2 with the given resolverPayload. +func NewRequest(resolverPayload ResolverPayload) Request { + return &BasicRequest{resolverPayload} +} + +var _ Request = &BasicRequest{} + +// Params are the map of parameters associated with this request +func (req *BasicRequest) ResolverPayload() ResolverPayload { + return req.resolverPayload +} + +// Requester is the interface implemented by a type that knows how to +// submit requests for remote resources. +type Requester interface { + // Submit accepts the name of a resolver to submit a request to + // along with the request itself. + Submit(ctx context.Context, name ResolverName, req Request) (ResolvedResource, error) +} + +// Request is implemented by any type that represents a single request +// for a remote resource. Implementing this interface gives the underlying +// type an opportunity to control properties such as whether the name of +// a request has particular properties, whether the request should be made +// to a specific namespace, and precisely which parameters should be included. +type Request interface { + ResolverPayload() ResolverPayload +} + +// ResolverPayload is the struct which holds the payload to create +// the Resolution Request CRD. +type ResolverPayload struct { + Name string + Namespace string + ResolutionSpec *v1beta1.ResolutionRequestSpec +} + +// ResolutionRequester is the interface implemented by a type that knows how to +// submit requests for remote resources. +type ResolutionRequester interface { + // SubmitResolutionRequest accepts the name of a resolver to submit a request to + // along with the request itself. + SubmitResolutionRequest(ctx context.Context, name ResolverName, req RequestRemoteResource) (ResolvedResource, error) +} + +// RequestRemoteResource is implemented by any type that represents a single request +// for a remote resource. Implementing this interface gives the underlying +// type an opportunity to control properties such as whether the name of +// a request has particular properties, whether the request should be made +// to a specific namespace, and precisely which parameters should be included. +type RequestRemoteResource interface { + ResolverPayload() ResolverPayload +} diff --git a/pkg/remoteresolution/resource/request_test.go b/pkg/remoteresolution/resource/request_test.go new file mode 100644 index 00000000000..e387070345b --- /dev/null +++ b/pkg/remoteresolution/resource/request_test.go @@ -0,0 +1,73 @@ +/* +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 resource_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + "github.com/tektoncd/pipeline/test/diff" +) + +func TestNewRequest(t *testing.T) { + type args struct { + resolverPayload resource.ResolverPayload + } + type want = args + golden := args{ + resolverPayload: resource.ResolverPayload{ + Name: "test-name", + Namespace: "test-namespace", + ResolutionSpec: &v1beta1.ResolutionRequestSpec{ + Params: v1.Params{ + {Name: "param1", Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "value1"}}, + {Name: "param2", Value: v1.ParamValue{Type: v1.ParamTypeString, StringVal: "value2"}}, + }, + }, + }, + } + tests := []struct { + name string + args args + want want + }{ + { + name: "empty", + args: args{}, + want: want{}, + }, + { + name: "all", + args: golden, + want: golden, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request := resource.NewRequest(tt.args.resolverPayload) + if request == nil { + t.Errorf("NewRequest() return nil") + } + if d := cmp.Diff(tt.want.resolverPayload, request.ResolverPayload()); d != "" { + t.Errorf("expected params to match %s", diff.PrintWantGot(d)) + } + }) + } +} diff --git a/pkg/remoteresolution/resource/resource.go b/pkg/remoteresolution/resource/resource.go new file mode 100644 index 00000000000..ff61f65b4fc --- /dev/null +++ b/pkg/remoteresolution/resource/resource.go @@ -0,0 +1,37 @@ +/* +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 resource + +import ( + "github.com/tektoncd/pipeline/pkg/resolution/common" +) + +// This is an alias for avoiding cycle import + +// ResolverName is the type used for a resolver's name and is mostly +// used to ensure the function signatures that accept it are clear on the +// purpose for the given string. +type ResolverName = common.ResolverName + +// OwnedRequest is implemented by any type implementing Request that also needs +// to express a Kubernetes OwnerRef relationship as part of the request being +// made. +type OwnedRequest = common.OwnedRequest + +// ResolvedResource is implemented by any type that offers a read-only +// view of the data and metadata of a resolved remote resource. +type ResolvedResource = common.ResolvedResource diff --git a/pkg/resolution/resolver/bundle/resolver.go b/pkg/resolution/resolver/bundle/resolver.go index a5cd07ac5f3..b23b2f7a959 100644 --- a/pkg/resolution/resolver/bundle/resolver.go +++ b/pkg/resolution/resolver/bundle/resolver.go @@ -25,7 +25,9 @@ import ( kauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "github.com/tektoncd/pipeline/pkg/resolution/common" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "k8s.io/client-go/kubernetes" "knative.dev/pkg/client/injection/kube/client" @@ -34,14 +36,14 @@ import ( const ( disabledError = "cannot handle resolution request, enable-bundles-resolver feature flag not true" - // LabelValueBundleResolverType is the value to use for the - // resolution.tekton.dev/type label on resource requests - LabelValueBundleResolverType string = "bundles" - // TODO(sbwsg): This should be exposed as a configurable option for // admins (e.g. via ConfigMap) timeoutDuration = time.Minute + // LabelValueBundleResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueBundleResolverType string = "bundles" + // BundleResolverName is the name that the bundle resolver should be associated with. BundleResolverName = "bundleresolver" ) @@ -76,21 +78,20 @@ func (r *Resolver) GetSelector(context.Context) map[string]string { // ValidateParams ensures parameters from a request are as expected. func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { - if r.isDisabled(ctx) { - return errors.New(disabledError) - } - if _, err := OptionsFromParams(ctx, params); err != nil { - return err - } - return nil + return ValidateParams(ctx, params) +} + +// Resolve uses the given params to resolve the requested file or resource. +func (r *Resolver) Resolve(ctx context.Context, params []v1.Param) (framework.ResolvedResource, error) { + return ResolveRequest(ctx, r.kubeClientSet, &v1beta1.ResolutionRequestSpec{Params: params}) } // Resolve uses the given params to resolve the requested file or resource. -func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (framework.ResolvedResource, error) { - if r.isDisabled(ctx) { +func ResolveRequest(ctx context.Context, kubeClientSet kubernetes.Interface, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) { + if isDisabled(ctx) { return nil, errors.New(disabledError) } - opts, err := OptionsFromParams(ctx, params) + opts, err := OptionsFromParams(ctx, req.Params) if err != nil { return nil, err } @@ -99,7 +100,7 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram imagePullSecrets = append(imagePullSecrets, opts.ImagePullSecret) } namespace := common.RequestNamespace(ctx) - kc, err := k8schain.New(ctx, r.kubeClientSet, k8schain.Options{ + kc, err := k8schain.New(ctx, kubeClientSet, k8schain.Options{ Namespace: namespace, ImagePullSecrets: imagePullSecrets, ServiceAccountName: kauth.NoServiceAccount, @@ -112,7 +113,17 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram return GetEntry(ctx, kc, opts) } -func (r *Resolver) isDisabled(ctx context.Context) bool { +func ValidateParams(ctx context.Context, params []pipelinev1.Param) error { + if isDisabled(ctx) { + return errors.New(disabledError) + } + if _, err := OptionsFromParams(ctx, params); err != nil { + return err + } + return nil +} + +func isDisabled(ctx context.Context) bool { cfg := resolverconfig.FromContextOrDefaults(ctx) return !cfg.FeatureFlags.EnableBundleResolver } diff --git a/pkg/resolution/resolver/bundle/resolver_test.go b/pkg/resolution/resolver/bundle/resolver_test.go index 4f575d00b8f..2cdc8571631 100644 --- a/pkg/resolution/resolver/bundle/resolver_test.go +++ b/pkg/resolution/resolver/bundle/resolver_test.go @@ -32,11 +32,11 @@ import ( pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" bundle "github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle" frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/internal" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" @@ -56,7 +56,7 @@ const ( func TestGetSelector(t *testing.T) { resolver := bundle.Resolver{} sel := resolver.GetSelector(context.Background()) - if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + if typ, has := sel[common.LabelKeyResolverType]; !has { t.Fatalf("unexpected selector: %v", sel) } else if typ != bundle.LabelValueBundleResolverType { t.Fatalf("unexpected type: %q", typ) @@ -347,7 +347,7 @@ func TestResolve(t *testing.T) { kind: "task", }, imageName: "single-task", - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), }, { name: "single task: param kind is capitalized, but kind in bundle is not", args: ¶ms{ @@ -357,7 +357,7 @@ func TestResolve(t *testing.T) { }, kindInBundle: "task", imageName: "single-task", - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), }, { name: "single task: tag is included in the bundle parameter", args: ¶ms{ @@ -366,7 +366,7 @@ func TestResolve(t *testing.T) { kind: "task", }, imageName: "single-task", - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), }, { name: "single task: using default kind value from configmap", args: ¶ms{ @@ -374,7 +374,7 @@ func TestResolve(t *testing.T) { name: "example-task", }, imageName: "single-task", - expectedStatus: internal.CreateResolutionRequestStatusWithData(taskAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(taskAsYAML), }, { name: "single pipeline", args: ¶ms{ @@ -383,7 +383,7 @@ func TestResolve(t *testing.T) { kind: "pipeline", }, imageName: "single-pipeline", - expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(pipelineAsYAML), }, { name: "multiple resources: an image has both task and pipeline resource", args: ¶ms{ @@ -392,7 +392,7 @@ func TestResolve(t *testing.T) { kind: "pipeline", }, imageName: "multiple-resources", - expectedStatus: internal.CreateResolutionRequestStatusWithData(pipelineAsYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(pipelineAsYAML), }, { name: "too many objects in an image", args: ¶ms{ @@ -400,7 +400,7 @@ func TestResolve(t *testing.T) { name: "2-task", kind: "task", }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErrMessage: fmt.Sprintf("contained more than the maximum %d allow objects", bundle.MaximumBundleObjects), }, { name: "single task no version", @@ -409,7 +409,7 @@ func TestResolve(t *testing.T) { name: "foo", kind: "task", }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundle.BundleAnnotationAPIVersion), }, { name: "single task no kind", @@ -418,7 +418,7 @@ func TestResolve(t *testing.T) { name: "foo", kind: "task", }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundle.BundleAnnotationKind), }, { name: "single task no name", @@ -427,7 +427,7 @@ func TestResolve(t *testing.T) { name: "foo", kind: "task", }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErrMessage: fmt.Sprintf("the layer 0 does not contain a %s annotation", bundle.BundleAnnotationName), }, { name: "single task kind incorrect form", @@ -436,7 +436,7 @@ func TestResolve(t *testing.T) { name: "foo", kind: "task", }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErrMessage: fmt.Sprintf("the layer 0 the annotation %s must be lowercased and singular, found %s", bundle.BundleAnnotationKind, "Task"), }, } @@ -521,7 +521,7 @@ func createRequest(p *params) *v1beta1.ResolutionRequest { Namespace: "foo", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: map[string]string{ - resolutioncommon.LabelKeyResolverType: bundle.LabelValueBundleResolverType, + common.LabelKeyResolverType: bundle.LabelValueBundleResolverType, }, }, Spec: v1beta1.ResolutionRequestSpec{ @@ -544,7 +544,7 @@ func createRequest(p *params) *v1beta1.ResolutionRequest { } func createError(image, msg string) error { - return &resolutioncommon.GetResourceError{ + return &common.GetResourceError{ ResolverName: bundle.BundleResolverName, Key: "foo/rr", Original: fmt.Errorf("invalid tekton bundle %s, error: %s", image, msg), diff --git a/pkg/resolution/resolver/cluster/resolver.go b/pkg/resolution/resolver/cluster/resolver.go index 6483016b93b..f3781cbef33 100644 --- a/pkg/resolution/resolver/cluster/resolver.go +++ b/pkg/resolution/resolver/cluster/resolver.go @@ -27,7 +27,7 @@ import ( pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" 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" + common "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" @@ -71,25 +71,24 @@ func (r *Resolver) GetName(_ context.Context) string { // the cluster resolver to process them. func (r *Resolver) GetSelector(_ context.Context) map[string]string { return map[string]string{ - resolutioncommon.LabelKeyResolverType: LabelValueClusterResolverType, + common.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 []pipelinev1.Param) error { - if r.isDisabled(ctx) { - return errors.New(disabledError) - } - - _, err := populateParamsWithDefaults(ctx, params) - return err + return ValidateParams(ctx, params) } // Resolve performs the work of fetching a resource from a namespace with the given // parameters. func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1.Param) (framework.ResolvedResource, error) { - if r.isDisabled(ctx) { + return ResolveFromParams(ctx, origParams, r.pipelineClientSet) +} + +func ResolveFromParams(ctx context.Context, origParams []pipelinev1.Param, pipelineClientSet clientset.Interface) (framework.ResolvedResource, error) { + if isDisabled(ctx) { return nil, errors.New(disabledError) } @@ -109,52 +108,23 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1.Param) ( switch params[KindParam] { case "task": - task, err := r.pipelineClientSet.TektonV1().Tasks(params[NamespaceParam]).Get(ctx, params[NameParam], metav1.GetOptions{}) + task, err := pipelineClientSet.TektonV1().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 } - uid = string(task.UID) - task.Kind = "Task" - task.APIVersion = groupVersion - 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 - } - sha256Checksum, err = task.Checksum() - if err != nil { - return nil, err - } - - spec, err = yaml.Marshal(task.Spec) + uid, data, sha256Checksum, spec, err = fetchTask(ctx, groupVersion, task, params) if err != nil { - logger.Infof("failed to marshal the spec of the task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } case "pipeline": - pipeline, err := r.pipelineClientSet.TektonV1().Pipelines(params[NamespaceParam]).Get(ctx, params[NameParam], metav1.GetOptions{}) + pipeline, err := pipelineClientSet.TektonV1().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 } - uid = string(pipeline.UID) - pipeline.Kind = "Pipeline" - pipeline.APIVersion = groupVersion - data, err = yaml.Marshal(pipeline) + uid, data, sha256Checksum, spec, err = fetchPipeline(ctx, groupVersion, pipeline, params) if err != nil { - logger.Infof("failed to marshal pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) - return nil, err - } - - sha256Checksum, err = pipeline.Checksum() - if err != nil { - return nil, err - } - - spec, err = yaml.Marshal(pipeline.Spec) - if err != nil { - logger.Infof("failed to marshal the spec of the pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) return nil, err } default: @@ -179,11 +149,6 @@ func (r *Resolver) GetConfigName(context.Context) string { return configMapName } -func (r *Resolver) isDisabled(ctx context.Context) bool { - cfg := resolverconfig.FromContextOrDefaults(ctx) - return !cfg.FeatureFlags.EnableClusterResolver -} - // ResolvedClusterResource implements framework.ResolvedResource and returns // the resolved file []byte data and an annotation map for any metadata. type ResolvedClusterResource struct { @@ -302,3 +267,62 @@ func isInCommaSeparatedList(checkVal string, commaList string) bool { } return false } +func isDisabled(ctx context.Context) bool { + cfg := resolverconfig.FromContextOrDefaults(ctx) + return !cfg.FeatureFlags.EnableClusterResolver +} + +func ValidateParams(ctx context.Context, params []pipelinev1.Param) error { + if isDisabled(ctx) { + return errors.New(disabledError) + } + + _, err := populateParamsWithDefaults(ctx, params) + return err +} + +func fetchTask(ctx context.Context, groupVersion string, task *pipelinev1.Task, params map[string]string) (string, []byte, []byte, []byte, error) { + logger := logging.FromContext(ctx) + uid := string(task.UID) + task.Kind = "Task" + task.APIVersion = groupVersion + 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, nil, nil, err + } + sha256Checksum, err := task.Checksum() + if err != nil { + return "", nil, nil, nil, err + } + + spec, err := yaml.Marshal(task.Spec) + if err != nil { + logger.Infof("failed to marshal the spec of the task %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return "", nil, nil, nil, err + } + return uid, data, sha256Checksum, spec, nil +} +func fetchPipeline(ctx context.Context, groupVersion string, pipeline *pipelinev1.Pipeline, params map[string]string) (string, []byte, []byte, []byte, error) { + logger := logging.FromContext(ctx) + uid := string(pipeline.UID) + pipeline.Kind = "Pipeline" + pipeline.APIVersion = groupVersion + 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, nil, nil, err + } + + sha256Checksum, err := pipeline.Checksum() + if err != nil { + return "", nil, nil, nil, err + } + + spec, err := yaml.Marshal(pipeline.Spec) + if err != nil { + logger.Infof("failed to marshal the spec of the pipeline %s from namespace %s: %v", params[NameParam], params[NamespaceParam], err) + return "", nil, nil, nil, err + } + return uid, data, sha256Checksum, spec, nil +} diff --git a/pkg/resolution/resolver/cluster/resolver_test.go b/pkg/resolution/resolver/cluster/resolver_test.go index e1051a7089d..6cfdf36ac70 100644 --- a/pkg/resolution/resolver/cluster/resolver_test.go +++ b/pkg/resolution/resolver/cluster/resolver_test.go @@ -30,12 +30,12 @@ import ( pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" cluster "github.com/tektoncd/pipeline/pkg/resolution/resolver/cluster" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/internal" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" @@ -53,7 +53,7 @@ const ( func TestGetSelector(t *testing.T) { resolver := cluster.Resolver{} sel := resolver.GetSelector(context.Background()) - if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + if typ, has := sel[common.LabelKeyResolverType]; !has { t.Fatalf("unexpected selector: %v", sel) } else if typ != cluster.LabelValueClusterResolverType { t.Fatalf("unexpected type: %q", typ) @@ -360,8 +360,8 @@ func TestResolve(t *testing.T) { kind: "task", resourceName: exampleTask.Name, namespace: "other-ns", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), - expectedErr: &resolutioncommon.GetResourceError{ + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &common.GetResourceError{ ResolverName: cluster.ClusterResolverName, Key: "foo/rr", Original: errors.New(`tasks.tekton.dev "example-task" not found`), @@ -372,8 +372,8 @@ func TestResolve(t *testing.T) { resourceName: exampleTask.Name, namespace: "other-ns", allowedNamespaces: "foo,bar", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), - expectedErr: &resolutioncommon.InvalidRequestError{ + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &common.InvalidRequestError{ ResolutionRequestKey: "foo/rr", Message: "access to specified namespace other-ns is not allowed", }, @@ -383,8 +383,8 @@ func TestResolve(t *testing.T) { resourceName: exampleTask.Name, namespace: "other-ns", blockedNamespaces: "foo,other-ns,bar", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), - expectedErr: &resolutioncommon.InvalidRequestError{ + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), + expectedErr: &common.InvalidRequestError{ ResolutionRequestKey: "foo/rr", Message: "access to specified namespace other-ns is blocked", }, @@ -471,7 +471,7 @@ func createRequest(kind, name, namespace string) *v1beta1.ResolutionRequest { Namespace: "foo", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: map[string]string{ - resolutioncommon.LabelKeyResolverType: cluster.LabelValueClusterResolverType, + common.LabelKeyResolverType: cluster.LabelValueClusterResolverType, }, }, Spec: v1beta1.ResolutionRequestSpec{ diff --git a/pkg/resolution/resolver/framework/controller.go b/pkg/resolution/resolver/framework/controller.go index f1d270a398a..61bc3c04626 100644 --- a/pkg/resolution/resolver/framework/controller.go +++ b/pkg/resolution/resolver/framework/controller.go @@ -46,7 +46,7 @@ type ReconcilerModifier = func(reconciler *Reconciler) // This sets up a lot of the boilerplate that individual resolvers // shouldn't need to be concerned with since it's common to all of them. func NewController(ctx context.Context, resolver Resolver, modifiers ...ReconcilerModifier) func(context.Context, configmap.Watcher) *controller.Impl { - if err := validateResolver(ctx, resolver); err != nil { + if err := ValidateResolver(ctx, resolver.GetSelector(ctx)); err != nil { panic(err.Error()) } return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl { @@ -60,7 +60,7 @@ func NewController(ctx context.Context, resolver Resolver, modifiers ...Reconcil } r := &Reconciler{ - LeaderAwareFuncs: leaderAwareFuncs(rrInformer.Lister()), + LeaderAwareFuncs: LeaderAwareFuncs(rrInformer.Lister()), kubeClientSet: kubeclientset, resolutionRequestLister: rrInformer.Lister(), resolutionRequestClientSet: rrclientset, @@ -82,7 +82,7 @@ func NewController(ctx context.Context, resolver Resolver, modifiers ...Reconcil }) _, err := rrInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ - FilterFunc: filterResolutionRequestsBySelector(resolver.GetSelector(ctx)), + FilterFunc: FilterResolutionRequestsBySelector(resolver.GetSelector(ctx)), Handler: cache.ResourceEventHandlerFuncs{ AddFunc: impl.Enqueue, UpdateFunc: func(oldObj, newObj interface{}) { @@ -101,7 +101,35 @@ func NewController(ctx context.Context, resolver Resolver, modifiers ...Reconcil } } -func filterResolutionRequestsBySelector(selector map[string]string) func(obj interface{}) bool { +// watchConfigChanges binds a framework.Resolver to updates on its +// configmap, using knative's configmap helpers. This is only done if +// the resolver implements the framework.ConfigWatcher interface. +func watchConfigChanges(ctx context.Context, reconciler *Reconciler, cmw configmap.Watcher) { + if configWatcher, ok := reconciler.resolver.(ConfigWatcher); ok { + logger := logging.FromContext(ctx) + resolverConfigName := configWatcher.GetConfigName(ctx) + if resolverConfigName == "" { + panic("resolver returned empty config name") + } + reconciler.configStore = NewConfigStore(resolverConfigName, logger) + reconciler.configStore.WatchConfigs(cmw) + } +} + +// applyModifiersAndDefaults applies the given modifiers to +// a reconciler and, after doing so, sets any default values for things +// that weren't set by a modifier. +func applyModifiersAndDefaults(ctx context.Context, r *Reconciler, modifiers []ReconcilerModifier) { + for _, mod := range modifiers { + mod(r) + } + + if r.Clock == nil { + r.Clock = clock.RealClock{} + } +} + +func FilterResolutionRequestsBySelector(selector map[string]string) func(obj interface{}) bool { return func(obj interface{}) bool { rr, ok := obj.(*v1beta1.ResolutionRequest) if !ok { @@ -127,7 +155,7 @@ func filterResolutionRequestsBySelector(selector map[string]string) func(obj int // fact that the controller crashes if they're missing. It looks // like this is bucketing based on labels. Should we use the filter // selector from above in the call to lister.List here? -func leaderAwareFuncs(lister rrlister.ResolutionRequestLister) reconciler.LeaderAwareFuncs { +func LeaderAwareFuncs(lister rrlister.ResolutionRequestLister) reconciler.LeaderAwareFuncs { return reconciler.LeaderAwareFuncs{ PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error { all, err := lister.List(labels.Everything()) @@ -156,8 +184,7 @@ var ( ErrorMissingTypeSelector = ErrMissingTypeSelector ) -func validateResolver(ctx context.Context, r Resolver) error { - sel := r.GetSelector(ctx) +func ValidateResolver(ctx context.Context, sel map[string]string) error { if sel == nil { return ErrMissingTypeSelector } @@ -166,31 +193,3 @@ func validateResolver(ctx context.Context, r Resolver) error { } return nil } - -// watchConfigChanges binds a framework.Resolver to updates on its -// configmap, using knative's configmap helpers. This is only done if -// the resolver implements the framework.ConfigWatcher interface. -func watchConfigChanges(ctx context.Context, reconciler *Reconciler, cmw configmap.Watcher) { - if configWatcher, ok := reconciler.resolver.(ConfigWatcher); ok { - logger := logging.FromContext(ctx) - resolverConfigName := configWatcher.GetConfigName(ctx) - if resolverConfigName == "" { - panic("resolver returned empty config name") - } - reconciler.configStore = NewConfigStore(resolverConfigName, logger) - reconciler.configStore.WatchConfigs(cmw) - } -} - -// applyModifiersAndDefaults applies the given modifiers to -// a reconciler and, after doing so, sets any default values for things -// that weren't set by a modifier. -func applyModifiersAndDefaults(ctx context.Context, r *Reconciler, modifiers []ReconcilerModifier) { - for _, mod := range modifiers { - mod(r) - } - - if r.Clock == nil { - r.Clock = clock.RealClock{} - } -} diff --git a/pkg/resolution/resolver/framework/fakeresolver.go b/pkg/resolution/resolver/framework/fakeresolver.go index 0943199601b..3fd363f825c 100644 --- a/pkg/resolution/resolver/framework/fakeresolver.go +++ b/pkg/resolution/resolver/framework/fakeresolver.go @@ -103,6 +103,10 @@ func (r *FakeResolver) GetSelector(_ context.Context) map[string]string { // ValidateParams returns an error if the given parameter map is not // valid for a resource request targeting the fake resolver. func (r *FakeResolver) ValidateParams(_ context.Context, params []pipelinev1.Param) error { + return ValidateParams(params) +} + +func ValidateParams(params []pipelinev1.Param) error { paramsMap := make(map[string]pipelinev1.ParamValue) for _, p := range params { paramsMap[p.Name] = p.Value @@ -132,6 +136,10 @@ func (r *FakeResolver) ValidateParams(_ context.Context, params []pipelinev1.Par // Resolve performs the work of fetching a file from the fake resolver given a map of // parameters. func (r *FakeResolver) Resolve(_ context.Context, params []pipelinev1.Param) (ResolvedResource, error) { + return Resolve(params, r.ForParam) +} + +func Resolve(params []pipelinev1.Param, forParam map[string]*FakeResolvedResource) (ResolvedResource, error) { paramsMap := make(map[string]pipelinev1.ParamValue) for _, p := range params { paramsMap[p.Name] = p.Value @@ -139,7 +147,7 @@ func (r *FakeResolver) Resolve(_ context.Context, params []pipelinev1.Param) (Re paramValue := paramsMap[FakeParamName].StringVal - frr, ok := r.ForParam[paramValue] + frr, ok := forParam[paramValue] if !ok { return nil, fmt.Errorf("couldn't find resource for param value %s", paramValue) } @@ -159,8 +167,13 @@ var _ TimedResolution = &FakeResolver{} // GetResolutionTimeout returns the configured timeout for the reconciler, or the default time.Duration if not configured. func (r *FakeResolver) GetResolutionTimeout(ctx context.Context, defaultTimeout time.Duration) time.Duration { - if r.Timeout > 0 { - return r.Timeout + return GetResolutionTimeout(r.Timeout, defaultTimeout) +} + +// GetResolutionTimeout returns the input timeout if set to something greater than 0 or the default time.Duration if not configured. +func GetResolutionTimeout(timeout, defaultTimeout time.Duration) time.Duration { + if timeout > 0 { + return timeout } return defaultTimeout } diff --git a/pkg/resolution/resolver/git/config.go b/pkg/resolution/resolver/git/config.go index a085bdfac39..44645b1fae6 100644 --- a/pkg/resolution/resolver/git/config.go +++ b/pkg/resolution/resolver/git/config.go @@ -17,20 +17,20 @@ limitations under the License. package git const ( - // defaultTimeoutKey is the configuration field name for controlling + // DefaultTimeoutKey is the configuration field name for controlling // the maximum duration of a resolution request for a file from git. - defaultTimeoutKey = "fetch-timeout" + DefaultTimeoutKey = "fetch-timeout" - // defaultURLKey is the configuration field name for controlling + // DefaultURLKey is the configuration field name for controlling // the git url to fetch the remote resource from. - defaultURLKey = "default-url" + DefaultURLKey = "default-url" - // defaultRevisionKey is the configuration field name for controlling + // DefaultRevisionKey is the configuration field name for controlling // the revision to fetch the remote resource from. - defaultRevisionKey = "default-revision" + DefaultRevisionKey = "default-revision" - // defaultOrgKey is the configuration field name for setting a default organization when using the SCM API. - defaultOrgKey = "default-org" + // DefaultOrgKey is the configuration field name for setting a default organization when using the SCM API. + DefaultOrgKey = "default-org" // ServerURLKey is the config map key for the SCM provider URL ServerURLKey = "server-url" diff --git a/pkg/resolution/resolver/git/params.go b/pkg/resolution/resolver/git/params.go index 679d0b0e9f5..9ca9248a4f4 100644 --- a/pkg/resolution/resolver/git/params.go +++ b/pkg/resolution/resolver/git/params.go @@ -17,24 +17,24 @@ limitations under the License. package git const ( - // urlParam is the git repo url when using the anonymous/full clone approach - urlParam string = "url" - // orgParam is the organization to find the repository in when using the SCM API approach - orgParam = "org" - // repoParam is the repository to use when using the SCM API approach - repoParam = "repo" - // pathParam is the pathInRepo into the git repo where a file is located. This is used with both approaches. - pathParam string = "pathInRepo" - // revisionParam is the git revision that a file should be fetched from. This is used with both approaches. - revisionParam string = "revision" - // tokenParam is an optional reference to a secret name for SCM API authentication - tokenParam string = "token" - // tokenKeyParam is an optional reference to a key in the tokenParam secret for SCM API authentication - tokenKeyParam string = "tokenKey" - // defaultTokenKeyParam is the default key in the tokenParam secret for SCM API authentication - defaultTokenKeyParam string = "token" - // scmTypeParams is an optional string overriding the scm-type configuration (ie: github, gitea, gitlab etc..) - scmTypeParam string = "scmType" - // serverURLParams is an optional string to the server URL for the SCM API to connect to - serverURLParam string = "serverURL" + // UrlParam is the git repo Url when using the anonymous/full clone approach + UrlParam string = "url" + // OrgParam is the organization to find the repository in when using the SCM API approach + OrgParam = "org" + // RepoParam is the repository to use when using the SCM API approach + RepoParam = "repo" + // PathParam is the pathInRepo into the git repo where a file is located. This is used with both approaches. + PathParam string = "pathInRepo" + // RevisionParam is the git revision that a file should be fetched from. This is used with both approaches. + RevisionParam string = "revision" + // TokenParam is an optional reference to a secret name for SCM API authentication + TokenParam string = "token" + // TokenKeyParam is an optional reference to a key in the TokenParam secret for SCM API authentication + TokenKeyParam string = "tokenKey" + // DefaultTokenKeyParam is the default key in the TokenParam secret for SCM API authentication + DefaultTokenKeyParam string = "token" + // scmTypeParam is an optional string overriding the scm-type configuration (ie: github, gitea, gitlab etc..) + ScmTypeParam string = "scmType" + // serverURLParam is an optional string to the server URL for the SCM API to connect to + ServerURLParam string = "serverURL" ) diff --git a/pkg/resolution/resolver/git/resolver.go b/pkg/resolution/resolver/git/resolver.go index 34fcd6ad18f..c8294de862f 100644 --- a/pkg/resolution/resolver/git/resolver.go +++ b/pkg/resolution/resolver/git/resolver.go @@ -36,8 +36,7 @@ import ( "github.com/jenkins-x/go-scm/scm/factory" resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "github.com/tektoncd/pipeline/pkg/resolution/common" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "go.uber.org/zap" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -106,41 +105,45 @@ func (r *Resolver) GetName(_ context.Context) string { // the gitresolver to process them. func (r *Resolver) GetSelector(_ context.Context) map[string]string { return map[string]string{ - resolutioncommon.LabelKeyResolverType: labelValueGitResolverType, + common.LabelKeyResolverType: labelValueGitResolverType, } } // ValidateParams returns an error if the given parameter map is not // valid for a resource request targeting the gitresolver. func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { - if r.isDisabled(ctx) { - return errors.New(disabledError) - } - - _, err := populateDefaultParams(ctx, params) - if err != nil { - return err - } - return nil + return ValidateParams(ctx, params) } // Resolve performs the work of fetching a file from git given a map of // parameters. func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1.Param) (framework.ResolvedResource, error) { - if r.isDisabled(ctx) { + if IsDisabled(ctx) { return nil, errors.New(disabledError) } - params, err := populateDefaultParams(ctx, origParams) + params, err := PopulateDefaultParams(ctx, origParams) if err != nil { return nil, err } - if params[urlParam] != "" { - return r.resolveAnonymousGit(ctx, params) + if params[UrlParam] != "" { + return ResolveAnonymousGit(ctx, params) } - return r.resolveAPIGit(ctx, params) + return ResolveAPIGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl, r.clientFunc) +} + +func ValidateParams(ctx context.Context, params []pipelinev1.Param) error { + if IsDisabled(ctx) { + return errors.New(disabledError) + } + + _, err := PopulateDefaultParams(ctx, params) + if err != nil { + return err + } + return nil } // validateRepoURL validates if the given URL is a valid git, http, https URL or @@ -152,81 +155,19 @@ func validateRepoURL(url string) bool { return re.MatchString(url) } -func (r *Resolver) resolveAPIGit(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) { - // If we got here, the "repo" param was specified, so use the API approach - scmType, serverURL, err := r.getSCMTypeAndServerURL(ctx, params) - if err != nil { - return nil, err - } - secretRef := &secretCacheKey{ - name: params[tokenParam], - key: params[tokenKeyParam], - } - if secretRef.name != "" { - if secretRef.key == "" { - secretRef.key = defaultTokenKeyParam - } - secretRef.ns = common.RequestNamespace(ctx) - } else { - secretRef = nil - } - apiToken, err := r.getAPIToken(ctx, secretRef) - if err != nil { - return nil, err - } - scmClient, err := r.clientFunc(scmType, serverURL, string(apiToken)) - if err != nil { - return nil, fmt.Errorf("failed to create SCM client: %w", err) - } - - orgRepo := fmt.Sprintf("%s/%s", params[orgParam], params[repoParam]) - path := params[pathParam] - ref := params[revisionParam] - - // fetch the actual content from a file in the repo - content, _, err := scmClient.Contents.Find(ctx, orgRepo, path, ref) - if err != nil { - return nil, fmt.Errorf("couldn't fetch resource content: %w", err) - } - if content == nil || len(content.Data) == 0 { - return nil, fmt.Errorf("no content for resource in %s %s", orgRepo, path) - } - - // find the actual git commit sha by the ref - commit, _, err := scmClient.Git.FindCommit(ctx, orgRepo, ref) - if err != nil || commit == nil { - return nil, fmt.Errorf("couldn't fetch the commit sha for the ref %s in the repo: %w", ref, err) - } - - // fetch the repository URL - repo, _, err := scmClient.Repositories.Find(ctx, orgRepo) - if err != nil { - return nil, fmt.Errorf("couldn't fetch repository: %w", err) - } - - return &resolvedGitResource{ - Content: content.Data, - Revision: commit.Sha, - Org: params[orgParam], - Repo: params[repoParam], - Path: content.Path, - URL: repo.Clone, - }, nil -} - -func (r *Resolver) resolveAnonymousGit(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) { +func ResolveAnonymousGit(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) { conf := framework.GetResolverConfigFromContext(ctx) - repo := params[urlParam] + repo := params[UrlParam] if repo == "" { - if urlString, ok := conf[defaultURLKey]; ok { + if urlString, ok := conf[DefaultURLKey]; ok { repo = urlString } else { return nil, errors.New("default Git Repo Url was not set during installation of the git resolver") } } - revision := params[revisionParam] + revision := params[RevisionParam] if revision == "" { - if revisionString, ok := conf[defaultRevisionKey]; ok { + if revisionString, ok := conf[DefaultRevisionKey]; ok { revision = revisionString } else { return nil, errors.New("default Git Revision was not set during installation of the git resolver") @@ -271,7 +212,7 @@ func (r *Resolver) resolveAnonymousGit(ctx context.Context, params map[string]st return nil, fmt.Errorf("checkout error: %w", err) } - path := params[pathParam] + path := params[PathParam] f, err := filesystem.Open(path) if err != nil { @@ -287,8 +228,8 @@ func (r *Resolver) resolveAnonymousGit(ctx context.Context, params map[string]st return &resolvedGitResource{ Revision: h.String(), Content: buf.Bytes(), - URL: params[urlParam], - Path: params[pathParam], + URL: params[UrlParam], + Path: params[PathParam], }, nil } @@ -306,7 +247,7 @@ var _ framework.TimedResolution = &Resolver{} // fetch-timeout field in the git-resolver-config configmap. func (r *Resolver) GetResolutionTimeout(ctx context.Context, defaultTimeout time.Duration) time.Duration { conf := framework.GetResolverConfigFromContext(ctx) - if timeoutString, ok := conf[defaultTimeoutKey]; ok { + if timeoutString, ok := conf[DefaultTimeoutKey]; ok { timeout, err := time.ParseDuration(timeoutString) if err == nil { return timeout @@ -315,9 +256,66 @@ func (r *Resolver) GetResolutionTimeout(ctx context.Context, defaultTimeout time return defaultTimeout } -func (r *Resolver) isDisabled(ctx context.Context) bool { - cfg := resolverconfig.FromContextOrDefaults(ctx) - return !cfg.FeatureFlags.EnableGitResolver +func PopulateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { + conf := framework.GetResolverConfigFromContext(ctx) + + paramsMap := make(map[string]string) + for _, p := range params { + paramsMap[p.Name] = p.Value.StringVal + } + + var missingParams []string + + if _, ok := paramsMap[RevisionParam]; !ok { + if defaultRevision, ok := conf[DefaultRevisionKey]; ok { + paramsMap[RevisionParam] = defaultRevision + } else { + missingParams = append(missingParams, RevisionParam) + } + } + if _, ok := paramsMap[PathParam]; !ok { + missingParams = append(missingParams, PathParam) + } + + if paramsMap[UrlParam] != "" && paramsMap[RepoParam] != "" { + return nil, fmt.Errorf("cannot specify both '%s' and '%s'", UrlParam, RepoParam) + } + + if paramsMap[UrlParam] == "" && paramsMap[RepoParam] == "" { + if urlString, ok := conf[DefaultURLKey]; ok { + paramsMap[UrlParam] = urlString + } else { + return nil, fmt.Errorf("must specify one of '%s' or '%s'", UrlParam, RepoParam) + } + } + + if paramsMap[RepoParam] != "" { + if _, ok := paramsMap[OrgParam]; !ok { + if defaultOrg, ok := conf[DefaultOrgKey]; ok { + paramsMap[OrgParam] = defaultOrg + } else { + return nil, fmt.Errorf("'%s' is required when '%s' is specified", OrgParam, RepoParam) + } + } + } + if len(missingParams) > 0 { + return nil, fmt.Errorf("missing required git resolver params: %s", strings.Join(missingParams, ", ")) + } + + // validate the url params if we are not using the SCM API + if paramsMap[RepoParam] == "" && paramsMap[OrgParam] == "" && !validateRepoURL(paramsMap[UrlParam]) { + return nil, fmt.Errorf("invalid git repository url: %s", paramsMap[UrlParam]) + } + + // TODO(sbwsg): validate pathInRepo is valid relative pathInRepo + return paramsMap, nil +} + +// supports the SPDX format which is recommended by in-toto +// ref: https://spdx.dev/spdx-specification-21-web-version/#h.49x2ik5 +// ref: https://github.com/in-toto/attestation/blob/main/spec/field_types.md +func spdxGit(url string) string { + return "git+" + url } // resolvedGitResource implements framework.ResolvedResource and returns @@ -342,10 +340,10 @@ func (r *resolvedGitResource) Data() []byte { // from git. func (r *resolvedGitResource) Annotations() map[string]string { m := map[string]string{ - AnnotationKeyRevision: r.Revision, - AnnotationKeyPath: r.Path, - AnnotationKeyURL: r.URL, - resolutioncommon.AnnotationKeyContentType: yamlContentType, + AnnotationKeyRevision: r.Revision, + AnnotationKeyPath: r.Path, + AnnotationKeyURL: r.URL, + common.AnnotationKeyContentType: yamlContentType, } if r.Org != "" { @@ -376,34 +374,69 @@ type secretCacheKey struct { key string } -func (r *Resolver) getSCMTypeAndServerURL(ctx context.Context, params map[string]string) (string, string, error) { - conf := framework.GetResolverConfigFromContext(ctx) - - var scmType, serverURL string - if key, ok := params[scmTypeParam]; ok { - scmType = key +func ResolveAPIGit(ctx context.Context, params map[string]string, kubeclient kubernetes.Interface, logger *zap.SugaredLogger, cache *cache.LRUExpireCache, ttl time.Duration, clientFunc func(string, string, string, ...factory.ClientOptionFunc) (*scm.Client, error)) (framework.ResolvedResource, error) { + // If we got here, the "repo" param was specified, so use the API approach + scmType, serverURL, err := getSCMTypeAndServerURL(ctx, params) + if err != nil { + return nil, err } - if scmType == "" { - if key, ok := conf[SCMTypeKey]; ok && scmType == "" { - scmType = key - } else { - return "", "", fmt.Errorf("missing or empty %s value in configmap", SCMTypeKey) + secretRef := &secretCacheKey{ + name: params[TokenParam], + key: params[TokenKeyParam], + } + if secretRef.name != "" { + if secretRef.key == "" { + secretRef.key = DefaultTokenKeyParam } + secretRef.ns = common.RequestNamespace(ctx) + } else { + secretRef = nil } - if key, ok := params[serverURLParam]; ok { - serverURL = key + apiToken, err := getAPIToken(ctx, secretRef, kubeclient, logger, cache, ttl) + if err != nil { + return nil, err } - if serverURL == "" { - if key, ok := conf[ServerURLKey]; ok && serverURL == "" { - serverURL = key - } else { - return "", "", fmt.Errorf("missing or empty %s value in configmap", ServerURLKey) - } + scmClient, err := clientFunc(scmType, serverURL, string(apiToken)) + if err != nil { + return nil, fmt.Errorf("failed to create SCM client: %w", err) } - return scmType, serverURL, nil + + orgRepo := fmt.Sprintf("%s/%s", params[OrgParam], params[RepoParam]) + path := params[PathParam] + ref := params[RevisionParam] + + // fetch the actual content from a file in the repo + content, _, err := scmClient.Contents.Find(ctx, orgRepo, path, ref) + if err != nil { + return nil, fmt.Errorf("couldn't fetch resource content: %w", err) + } + if content == nil || len(content.Data) == 0 { + return nil, fmt.Errorf("no content for resource in %s %s", orgRepo, path) + } + + // find the actual git commit sha by the ref + commit, _, err := scmClient.Git.FindCommit(ctx, orgRepo, ref) + if err != nil || commit == nil { + return nil, fmt.Errorf("couldn't fetch the commit sha for the ref %s in the repo: %w", ref, err) + } + + // fetch the repository URL + repo, _, err := scmClient.Repositories.Find(ctx, orgRepo) + if err != nil { + return nil, fmt.Errorf("couldn't fetch repository: %w", err) + } + + return &resolvedGitResource{ + Content: content.Data, + Revision: commit.Sha, + Org: params[OrgParam], + Repo: params[RepoParam], + Path: content.Path, + URL: repo.Clone, + }, nil } -func (r *Resolver) getAPIToken(ctx context.Context, apiSecret *secretCacheKey) ([]byte, error) { +func getAPIToken(ctx context.Context, apiSecret *secretCacheKey, kubeclient kubernetes.Interface, logger *zap.SugaredLogger, cache *cache.LRUExpireCache, ttl time.Duration) ([]byte, error) { conf := framework.GetResolverConfigFromContext(ctx) ok := false @@ -417,15 +450,15 @@ func (r *Resolver) getAPIToken(ctx context.Context, apiSecret *secretCacheKey) ( if apiSecret.name == "" { if apiSecret.name, ok = conf[APISecretNameKey]; !ok || apiSecret.name == "" { - err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", repoParam, APISecretNameKey) - r.logger.Info(err) + err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", RepoParam, APISecretNameKey) + logger.Info(err) return nil, err } } if apiSecret.key == "" { if apiSecret.key, ok = conf[APISecretKeyKey]; !ok || apiSecret.key == "" { - err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", repoParam, APISecretKeyKey) - r.logger.Info(err) + err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", RepoParam, APISecretKeyKey) + logger.Info(err) return nil, err } } @@ -436,94 +469,64 @@ func (r *Resolver) getAPIToken(ctx context.Context, apiSecret *secretCacheKey) ( } if cacheSecret { - val, ok := r.cache.Get(apiSecret) + val, ok := cache.Get(apiSecret) if ok { return val.([]byte), nil } } - secret, err := r.kubeClient.CoreV1().Secrets(apiSecret.ns).Get(ctx, apiSecret.name, metav1.GetOptions{}) + secret, err := kubeclient.CoreV1().Secrets(apiSecret.ns).Get(ctx, apiSecret.name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { notFoundErr := fmt.Errorf("cannot get API token, secret %s not found in namespace %s", apiSecret.name, apiSecret.ns) - r.logger.Info(notFoundErr) + logger.Info(notFoundErr) return nil, notFoundErr } wrappedErr := fmt.Errorf("error reading API token from secret %s in namespace %s: %w", apiSecret.name, apiSecret.ns, err) - r.logger.Info(wrappedErr) + logger.Info(wrappedErr) return nil, wrappedErr } secretVal, ok := secret.Data[apiSecret.key] if !ok { err := fmt.Errorf("cannot get API token, key %s not found in secret %s in namespace %s", apiSecret.key, apiSecret.name, apiSecret.ns) - r.logger.Info(err) + logger.Info(err) return nil, err } if cacheSecret { - r.cache.Add(apiSecret, secretVal, r.ttl) + cache.Add(apiSecret, secretVal, ttl) } return secretVal, nil } -func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { +func getSCMTypeAndServerURL(ctx context.Context, params map[string]string) (string, string, error) { conf := framework.GetResolverConfigFromContext(ctx) - paramsMap := make(map[string]string) - for _, p := range params { - paramsMap[p.Name] = p.Value.StringVal + var scmType, serverURL string + if key, ok := params[ScmTypeParam]; ok { + scmType = key } - - var missingParams []string - - if _, ok := paramsMap[revisionParam]; !ok { - if defaultRevision, ok := conf[defaultRevisionKey]; ok { - paramsMap[revisionParam] = defaultRevision + if scmType == "" { + if key, ok := conf[SCMTypeKey]; ok && scmType == "" { + scmType = key } else { - missingParams = append(missingParams, revisionParam) + return "", "", fmt.Errorf("missing or empty %s value in configmap", SCMTypeKey) } } - if _, ok := paramsMap[pathParam]; !ok { - missingParams = append(missingParams, pathParam) - } - - if paramsMap[urlParam] != "" && paramsMap[repoParam] != "" { - return nil, fmt.Errorf("cannot specify both '%s' and '%s'", urlParam, repoParam) + if key, ok := params[ServerURLParam]; ok { + serverURL = key } - - if paramsMap[urlParam] == "" && paramsMap[repoParam] == "" { - if urlString, ok := conf[defaultURLKey]; ok { - paramsMap[urlParam] = urlString + if serverURL == "" { + if key, ok := conf[ServerURLKey]; ok && serverURL == "" { + serverURL = key } else { - return nil, fmt.Errorf("must specify one of '%s' or '%s'", urlParam, repoParam) - } - } - - if paramsMap[repoParam] != "" { - if _, ok := paramsMap[orgParam]; !ok { - if defaultOrg, ok := conf[defaultOrgKey]; ok { - paramsMap[orgParam] = defaultOrg - } else { - return nil, fmt.Errorf("'%s' is required when '%s' is specified", orgParam, repoParam) - } + return "", "", fmt.Errorf("missing or empty %s value in configmap", ServerURLKey) } } - if len(missingParams) > 0 { - return nil, fmt.Errorf("missing required git resolver params: %s", strings.Join(missingParams, ", ")) - } - - // validate the url params if we are not using the SCM API - if paramsMap[repoParam] == "" && paramsMap[orgParam] == "" && !validateRepoURL(paramsMap[urlParam]) { - return nil, fmt.Errorf("invalid git repository url: %s", paramsMap[urlParam]) - } - - // TODO(sbwsg): validate pathInRepo is valid relative pathInRepo - return paramsMap, nil + return scmType, serverURL, nil } -// supports the SPDX format which is recommended by in-toto -// ref: https://spdx.dev/spdx-specification-21-web-version/#h.49x2ik5 -// ref: https://github.com/in-toto/attestation/blob/main/spec/field_types.md -func spdxGit(url string) string { - return "git+" + url +func IsDisabled(ctx context.Context) bool { + cfg := resolverconfig.FromContextOrDefaults(ctx) + return !cfg.FeatureFlags.EnableGitResolver } diff --git a/pkg/resolution/resolver/git/resolver_test.go b/pkg/resolution/resolver/git/resolver_test.go index a9f4c0490d9..7185abfe6aa 100644 --- a/pkg/resolution/resolver/git/resolver_test.go +++ b/pkg/resolution/resolver/git/resolver_test.go @@ -36,11 +36,11 @@ import ( resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/internal" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" @@ -52,7 +52,7 @@ import ( func TestGetSelector(t *testing.T) { resolver := Resolver{} sel := resolver.GetSelector(context.Background()) - if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + if typ, has := sel[common.LabelKeyResolverType]; !has { t.Fatalf("unexpected selector: %v", sel) } else if typ != labelValueGitResolverType { t.Fatalf("unexpected type: %q", typ) @@ -68,57 +68,57 @@ func TestValidateParams(t *testing.T) { { name: "params with revision", params: map[string]string{ - urlParam: "http://foo/bar/hello/moto", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "http://foo/bar/hello/moto", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "https url", params: map[string]string{ - urlParam: "https://foo/bar/hello/moto", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "https://foo/bar/hello/moto", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "https url with username password", params: map[string]string{ - urlParam: "https://user:pass@foo/bar/hello/moto", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "https://user:pass@foo/bar/hello/moto", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "git server url", params: map[string]string{ - urlParam: "git://repo/hello/moto", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "git://repo/hello/moto", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "git url from a local repository", params: map[string]string{ - urlParam: "/tmp/repo", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "/tmp/repo", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "git url from a git ssh repository", params: map[string]string{ - urlParam: "git@host.com:foo/bar", - pathParam: "bar", - revisionParam: "baz", + UrlParam: "git@host.com:foo/bar", + PathParam: "bar", + RevisionParam: "baz", }, }, { name: "bad url", params: map[string]string{ - urlParam: "foo://bar", - pathParam: "path", - revisionParam: "revision", + UrlParam: "foo://bar", + PathParam: "path", + RevisionParam: "revision", }, wantErr: "invalid git repository url: foo://bar", }, @@ -147,8 +147,8 @@ func TestValidateParamsNotEnabled(t *testing.T) { var err error someParams := map[string]string{ - pathParam: "bar", - revisionParam: "baz", + PathParam: "bar", + RevisionParam: "baz", } err = resolver.ValidateParams(resolverDisabledContext(), toParams(someParams)) if err == nil { @@ -168,32 +168,32 @@ func TestValidateParams_Failure(t *testing.T) { { name: "missing multiple", params: map[string]string{ - orgParam: "abcd1234", - repoParam: "foo", + OrgParam: "abcd1234", + RepoParam: "foo", }, - expectedErr: fmt.Sprintf("missing required git resolver params: %s, %s", revisionParam, pathParam), + expectedErr: fmt.Sprintf("missing required git resolver params: %s, %s", RevisionParam, PathParam), }, { name: "no repo or url", params: map[string]string{ - revisionParam: "abcd1234", - pathParam: "/foo/bar", + RevisionParam: "abcd1234", + PathParam: "/foo/bar", }, expectedErr: "must specify one of 'url' or 'repo'", }, { name: "both repo and url", params: map[string]string{ - revisionParam: "abcd1234", - pathParam: "/foo/bar", - urlParam: "http://foo", - repoParam: "foo", + RevisionParam: "abcd1234", + PathParam: "/foo/bar", + UrlParam: "http://foo", + RepoParam: "foo", }, expectedErr: "cannot specify both 'url' and 'repo'", }, { name: "no org with repo", params: map[string]string{ - revisionParam: "abcd1234", - pathParam: "/foo/bar", - repoParam: "foo", + RevisionParam: "abcd1234", + PathParam: "/foo/bar", + RepoParam: "foo", }, expectedErr: "'org' is required when 'repo' is specified", }, @@ -227,7 +227,7 @@ func TestGetResolutionTimeoutCustom(t *testing.T) { defaultTimeout := 30 * time.Minute configTimeout := 5 * time.Second config := map[string]string{ - defaultTimeoutKey: configTimeout.String(), + DefaultTimeoutKey: configTimeout.String(), } ctx := framework.InjectResolverConfigToContext(context.Background(), config) timeout := resolver.GetResolutionTimeout(ctx, defaultTimeout) @@ -242,8 +242,8 @@ func TestResolveNotEnabled(t *testing.T) { var err error someParams := map[string]string{ - pathParam: "bar", - revisionParam: "baz", + PathParam: "bar", + RevisionParam: "baz", } _, err = resolver.Resolve(resolverDisabledContext(), toParams(someParams)) if err == nil { @@ -348,7 +348,7 @@ func TestResolve(t *testing.T) { url: anonFakeRepoURL, }, expectedCommitSHA: commitSHAsInAnonRepo[2], - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), }, { name: "clone: revision is tag name", args: ¶ms{ @@ -357,7 +357,7 @@ func TestResolve(t *testing.T) { url: anonFakeRepoURL, }, expectedCommitSHA: commitSHAsInAnonRepo[2], - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), }, { name: "clone: revision is the full tag name i.e. refs/tags/v1", args: ¶ms{ @@ -366,7 +366,7 @@ func TestResolve(t *testing.T) { url: anonFakeRepoURL, }, expectedCommitSHA: commitSHAsInAnonRepo[2], - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")), }, { name: "clone: revision is a branch name", args: ¶ms{ @@ -375,7 +375,7 @@ func TestResolve(t *testing.T) { url: anonFakeRepoURL, }, expectedCommitSHA: commitSHAsInAnonRepo[1], - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte("new content in test branch")), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("new content in test branch")), }, { name: "clone: revision is a specific commit sha", args: ¶ms{ @@ -384,7 +384,7 @@ func TestResolve(t *testing.T) { url: anonFakeRepoURL, }, expectedCommitSHA: commitSHAsInAnonRepo[0], - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte("old content in test branch")), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("old content in test branch")), }, { name: "clone: file does not exist", args: ¶ms{ @@ -417,7 +417,7 @@ func TestResolve(t *testing.T) { }, apiToken: "some-token", expectedCommitSHA: commitSHAsInSCMRepo[0], - expectedStatus: internal.CreateResolutionRequestStatusWithData(mainTaskYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), }, { name: "api: successful task", args: ¶ms{ @@ -435,7 +435,7 @@ func TestResolve(t *testing.T) { }, apiToken: "some-token", expectedCommitSHA: commitSHAsInSCMRepo[0], - expectedStatus: internal.CreateResolutionRequestStatusWithData(mainTaskYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), }, { name: "api: successful pipeline", args: ¶ms{ @@ -453,7 +453,7 @@ func TestResolve(t *testing.T) { }, apiToken: "some-token", expectedCommitSHA: commitSHAsInSCMRepo[0], - expectedStatus: internal.CreateResolutionRequestStatusWithData(mainPipelineYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainPipelineYAML), }, { name: "api: successful pipeline with default revision", args: ¶ms{ @@ -467,11 +467,11 @@ func TestResolve(t *testing.T) { APISecretNameKey: "token-secret", APISecretKeyKey: "token", APISecretNamespaceKey: system.Namespace(), - defaultRevisionKey: "other", + DefaultRevisionKey: "other", }, apiToken: "some-token", expectedCommitSHA: commitSHAsInSCMRepo[1], - expectedStatus: internal.CreateResolutionRequestStatusWithData(otherPipelineYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(otherPipelineYAML), }, { name: "api: successful override scm type and server URL from user params", @@ -492,7 +492,7 @@ func TestResolve(t *testing.T) { }, apiToken: "some-token", expectedCommitSHA: commitSHAsInSCMRepo[0], - expectedStatus: internal.CreateResolutionRequestStatusWithData(mainTaskYAML), + expectedStatus: resolution.CreateResolutionRequestStatusWithData(mainTaskYAML), }, { name: "api: file does not exist", args: ¶ms{ @@ -509,7 +509,7 @@ func TestResolve(t *testing.T) { APISecretNamespaceKey: system.Namespace(), }, apiToken: "some-token", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErr: createError("couldn't fetch resource content: file testdata/test-org/test-repo/refs/main/pipelines/other-pipeline.yaml does not exist: stat testdata/test-org/test-repo/refs/main/pipelines/other-pipeline.yaml: no such file or directory"), }, { name: "api: token not found", @@ -526,7 +526,7 @@ func TestResolve(t *testing.T) { APISecretKeyKey: "token", APISecretNamespaceKey: system.Namespace(), }, - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErr: createError("cannot get API token, secret token-secret not found in namespace " + system.Namespace()), }, { name: "api: token secret name not specified", @@ -543,7 +543,7 @@ func TestResolve(t *testing.T) { APISecretNamespaceKey: system.Namespace(), }, apiToken: "some-token", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErr: createError("cannot get API token, required when specifying 'repo' param, 'api-token-secret-name' not specified in config"), }, { name: "api: token secret key not specified", @@ -560,7 +560,7 @@ func TestResolve(t *testing.T) { APISecretNamespaceKey: system.Namespace(), }, apiToken: "some-token", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErr: createError("cannot get API token, required when specifying 'repo' param, 'api-token-secret-key' not specified in config"), }, { name: "api: SCM type not specified", @@ -576,7 +576,7 @@ func TestResolve(t *testing.T) { APISecretNamespaceKey: system.Namespace(), }, apiToken: "some-token", - expectedStatus: internal.CreateResolutionRequestFailureStatus(), + expectedStatus: resolution.CreateResolutionRequestFailureStatus(), expectedErr: createError("missing or empty scm-type value in configmap"), }} @@ -588,9 +588,9 @@ func TestResolve(t *testing.T) { if cfg == nil { cfg = make(map[string]string) } - cfg[defaultTimeoutKey] = "1m" - if cfg[defaultRevisionKey] == "" { - cfg[defaultRevisionKey] = plumbing.Master.Short() + cfg[DefaultTimeoutKey] = "1m" + if cfg[DefaultRevisionKey] == "" { + cfg[DefaultRevisionKey] = plumbing.Master.Short() } request := createRequest(tc.args) @@ -623,7 +623,7 @@ func TestResolve(t *testing.T) { if expectedStatus.Annotations == nil { expectedStatus.Annotations = make(map[string]string) } - expectedStatus.Annotations[resolutioncommon.AnnotationKeyContentType] = "application/x-yaml" + expectedStatus.Annotations[common.AnnotationKeyContentType] = "application/x-yaml" expectedStatus.Annotations[AnnotationKeyRevision] = tc.expectedCommitSHA expectedStatus.Annotations[AnnotationKeyPath] = tc.args.pathInRepo @@ -819,12 +819,12 @@ func createRequest(args *params) *v1beta1.ResolutionRequest { Namespace: "foo", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: map[string]string{ - resolutioncommon.LabelKeyResolverType: labelValueGitResolverType, + common.LabelKeyResolverType: labelValueGitResolverType, }, }, Spec: v1beta1.ResolutionRequestSpec{ Params: []pipelinev1.Param{{ - Name: pathParam, + Name: PathParam, Value: *pipelinev1.NewStructuredValues(args.pathInRepo), }}, }, @@ -832,45 +832,45 @@ func createRequest(args *params) *v1beta1.ResolutionRequest { if args.revision != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: revisionParam, + Name: RevisionParam, Value: *pipelinev1.NewStructuredValues(args.revision), }) } if args.serverURL != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: serverURLParam, + Name: ServerURLParam, Value: *pipelinev1.NewStructuredValues(args.serverURL), }) } if args.scmType != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: scmTypeParam, + Name: ScmTypeParam, Value: *pipelinev1.NewStructuredValues(args.scmType), }) } if args.url != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: urlParam, + Name: UrlParam, Value: *pipelinev1.NewStructuredValues(args.url), }) } else { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: repoParam, + Name: RepoParam, Value: *pipelinev1.NewStructuredValues(args.repo), }) rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: orgParam, + Name: OrgParam, Value: *pipelinev1.NewStructuredValues(args.org), }) if args.token != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: tokenParam, + Name: TokenParam, Value: *pipelinev1.NewStructuredValues(args.token), }) rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: tokenKeyParam, + Name: TokenKeyParam, Value: *pipelinev1.NewStructuredValues(args.tokenKey), }) } @@ -884,7 +884,7 @@ func resolverDisabledContext() context.Context { } func createError(msg string) error { - return &resolutioncommon.GetResourceError{ + return &common.GetResourceError{ ResolverName: gitResolverName, Key: "foo/rr", Original: errors.New(msg), diff --git a/pkg/resolution/resolver/http/config.go b/pkg/resolution/resolver/http/config.go index 0685fdb07ba..c8ffd8ed545 100644 --- a/pkg/resolution/resolver/http/config.go +++ b/pkg/resolution/resolver/http/config.go @@ -17,7 +17,7 @@ limitations under the License. package http const ( - // timeoutKey is the configuration field name for controlling + // TimeoutKey is the configuration field name for controlling // the maximum duration of a resolution request for a file from http. - timeoutKey = "fetch-timeout" + TimeoutKey = "fetch-timeout" ) diff --git a/pkg/resolution/resolver/http/params.go b/pkg/resolution/resolver/http/params.go index b2e8c9a9c6c..d58008b5942 100644 --- a/pkg/resolution/resolver/http/params.go +++ b/pkg/resolution/resolver/http/params.go @@ -14,15 +14,15 @@ limitations under the License. package http const ( - // urlParam is the URL to fetch the task from - urlParam string = "url" + // UrlParam is the URL to fetch the task from + UrlParam string = "url" - // httpBasicAuthUsername is the user name to use for basic auth - httpBasicAuthUsername string = "http-username" + // HttpBasicAuthUsername is the user name to use for basic auth + HttpBasicAuthUsername string = "http-username" - // httpBasicAuthSecret is the reference to a secret in the PipelineRun or TaskRun namespace to use for basic auth - httpBasicAuthSecret string = "http-password-secret" + // HttpBasicAuthSecret is the reference to a secret in the PipelineRun or TaskRun namespace to use for basic auth + HttpBasicAuthSecret string = "http-password-secret" - // httpBasicAuthSecretKey is the key in the httpBasicAuthSecret secret to use for basic auth - httpBasicAuthSecretKey string = "http-password-secret-key" + // HttpBasicAuthSecretKey is the key in the httpBasicAuthSecret secret to use for basic auth + HttpBasicAuthSecretKey string = "http-password-secret-key" ) diff --git a/pkg/resolution/resolver/http/resolver.go b/pkg/resolution/resolver/http/resolver.go index 49d75bd2dd1..1b9c9e7c921 100644 --- a/pkg/resolution/resolver/http/resolver.go +++ b/pkg/resolution/resolver/http/resolver.go @@ -28,7 +28,7 @@ import ( resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "go.uber.org/zap" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -89,31 +89,24 @@ func (r *Resolver) GetSelector(context.Context) map[string]string { // ValidateParams ensures parameters from a request are as expected. func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { - if r.isDisabled(ctx) { - return errors.New(disabledError) - } - _, err := populateDefaultParams(ctx, params) - if err != nil { - return err - } - return nil + return ValidateParams(ctx, params) } // Resolve uses the given params to resolve the requested file or resource. func (r *Resolver) Resolve(ctx context.Context, oParams []pipelinev1.Param) (framework.ResolvedResource, error) { - if r.isDisabled(ctx) { + if IsDisabled(ctx) { return nil, errors.New(disabledError) } - params, err := populateDefaultParams(ctx, oParams) + params, err := PopulateDefaultParams(ctx, oParams) if err != nil { return nil, err } - return r.fetchHttpResource(ctx, params) + return FetchHttpResource(ctx, params, r.kubeClient, r.logger) } -func (r *Resolver) isDisabled(ctx context.Context) bool { +func IsDisabled(ctx context.Context) bool { cfg := resolverconfig.FromContextOrDefaults(ctx) return !cfg.FeatureFlags.EnableHttpResolver } @@ -151,7 +144,7 @@ func (rr *resolvedHttpResource) RefSource() *pipelinev1.RefSource { } } -func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { +func PopulateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { paramsMap := make(map[string]string) for _, p := range params { paramsMap[p.Name] = p.Value.StringVal @@ -159,33 +152,33 @@ func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[ var missingParams []string - if _, ok := paramsMap[urlParam]; !ok { - missingParams = append(missingParams, urlParam) + if _, ok := paramsMap[UrlParam]; !ok { + missingParams = append(missingParams, UrlParam) } else { - u, err := url.ParseRequestURI(paramsMap[urlParam]) + u, err := url.ParseRequestURI(paramsMap[UrlParam]) if err != nil { - return nil, fmt.Errorf("cannot parse url %s: %w", paramsMap[urlParam], err) + return nil, fmt.Errorf("cannot parse url %s: %w", paramsMap[UrlParam], err) } if u.Scheme != "http" && u.Scheme != "https" { - return nil, fmt.Errorf("url %s is not a valid http(s) url", paramsMap[urlParam]) + return nil, fmt.Errorf("url %s is not a valid http(s) url", paramsMap[UrlParam]) } } - if username, ok := paramsMap[httpBasicAuthUsername]; ok { - if _, ok := paramsMap[httpBasicAuthSecret]; !ok { - return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthSecret, httpBasicAuthUsername) + if username, ok := paramsMap[HttpBasicAuthUsername]; ok { + if _, ok := paramsMap[HttpBasicAuthSecret]; !ok { + return nil, fmt.Errorf("missing required param %s when using %s", HttpBasicAuthSecret, HttpBasicAuthUsername) } if username == "" { - return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthUsername) + return nil, fmt.Errorf("value %s cannot be empty", HttpBasicAuthUsername) } } - if secret, ok := paramsMap[httpBasicAuthSecret]; ok { - if _, ok := paramsMap[httpBasicAuthUsername]; !ok { - return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthUsername, httpBasicAuthSecret) + if secret, ok := paramsMap[HttpBasicAuthSecret]; ok { + if _, ok := paramsMap[HttpBasicAuthUsername]; !ok { + return nil, fmt.Errorf("missing required param %s when using %s", HttpBasicAuthUsername, HttpBasicAuthSecret) } if secret == "" { - return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthSecret) + return nil, fmt.Errorf("value %s cannot be empty", HttpBasicAuthSecret) } } @@ -199,7 +192,7 @@ func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[ func makeHttpClient(ctx context.Context) (*http.Client, error) { conf := framework.GetResolverConfigFromContext(ctx) timeout, _ := time.ParseDuration(defaultHttpTimeoutValue) - if v, ok := conf[timeoutKey]; ok { + if v, ok := conf[TimeoutKey]; ok { var err error timeout, err = time.ParseDuration(v) if err != nil { @@ -211,7 +204,7 @@ func makeHttpClient(ctx context.Context) (*http.Client, error) { }, nil } -func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) { +func FetchHttpResource(ctx context.Context, params map[string]string, kubeclient kubernetes.Interface, logger *zap.SugaredLogger) (framework.ResolvedResource, error) { var targetURL string var ok bool @@ -220,8 +213,8 @@ func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]stri return nil, err } - if targetURL, ok = params[urlParam]; !ok { - return nil, fmt.Errorf("missing required params: %s", urlParam) + if targetURL, ok = params[UrlParam]; !ok { + return nil, fmt.Errorf("missing required params: %s", UrlParam) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) @@ -230,8 +223,8 @@ func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]stri } // NOTE(chmouel): We already made sure that username and secret was specified by the user - if secret, ok := params[httpBasicAuthSecret]; ok && secret != "" { - if encodedSecret, err := r.getBasicAuthSecret(ctx, params); err != nil { + if secret, ok := params[HttpBasicAuthSecret]; ok && secret != "" { + if encodedSecret, err := getBasicAuthSecret(ctx, params, kubeclient, logger); err != nil { return nil, err } else { req.Header.Set("Authorization", encodedSecret) @@ -259,33 +252,44 @@ func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]stri }, nil } -func (r *Resolver) getBasicAuthSecret(ctx context.Context, params map[string]string) (string, error) { - secretName := params[httpBasicAuthSecret] - userName := params[httpBasicAuthUsername] +func getBasicAuthSecret(ctx context.Context, params map[string]string, kubeclient kubernetes.Interface, logger *zap.SugaredLogger) (string, error) { + secretName := params[HttpBasicAuthSecret] + userName := params[HttpBasicAuthUsername] tokenSecretKey := defaultBasicAuthSecretKey - if v, ok := params[httpBasicAuthSecretKey]; ok { + if v, ok := params[HttpBasicAuthSecretKey]; ok { if v != "" { tokenSecretKey = v } } secretNS := common.RequestNamespace(ctx) - secret, err := r.kubeClient.CoreV1().Secrets(secretNS).Get(ctx, secretName, metav1.GetOptions{}) + secret, err := kubeclient.CoreV1().Secrets(secretNS).Get(ctx, secretName, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { notFoundErr := fmt.Errorf("cannot get API token, secret %s not found in namespace %s", secretName, secretNS) - r.logger.Info(notFoundErr) + logger.Info(notFoundErr) return "", notFoundErr } wrappedErr := fmt.Errorf("error reading API token from secret %s in namespace %s: %w", secretName, secretNS, err) - r.logger.Info(wrappedErr) + logger.Info(wrappedErr) return "", wrappedErr } secretVal, ok := secret.Data[tokenSecretKey] if !ok { err := fmt.Errorf("cannot get API token, key %s not found in secret %s in namespace %s", tokenSecretKey, secretName, secretNS) - r.logger.Info(err) + logger.Info(err) return "", err } return "Basic " + base64.StdEncoding.EncodeToString( []byte(fmt.Sprintf("%s:%s", userName, secretVal))), nil } + +func ValidateParams(ctx context.Context, params []pipelinev1.Param) error { + if IsDisabled(ctx) { + return errors.New(disabledError) + } + _, err := PopulateDefaultParams(ctx, params) + if err != nil { + return err + } + return nil +} diff --git a/pkg/resolution/resolver/http/resolver_test.go b/pkg/resolution/resolver/http/resolver_test.go index 630b3882d6e..dddfab29dbf 100644 --- a/pkg/resolution/resolver/http/resolver_test.go +++ b/pkg/resolution/resolver/http/resolver_test.go @@ -33,11 +33,11 @@ import ( resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "github.com/tektoncd/pipeline/pkg/internal/resolution" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/internal" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" corev1 "k8s.io/api/core/v1" @@ -68,7 +68,7 @@ const emptyStr = "empty" func TestGetSelector(t *testing.T) { resolver := Resolver{} sel := resolver.GetSelector(context.Background()) - if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + if typ, has := sel[common.LabelKeyResolverType]; !has { t.Fatalf("unexpected selector: %v", sel) } else if typ != LabelValueHttpResolverType { t.Fatalf("unexpected type: %q", typ) @@ -104,7 +104,7 @@ func TestValidateParams(t *testing.T) { resolver := Resolver{} params := map[string]string{} if tc.url != "nourl" { - params[urlParam] = tc.url + params[UrlParam] = tc.url } err := resolver.ValidateParams(contextWithConfig(defaultHttpTimeoutValue), toParams(params)) if tc.expectedErr != nil { @@ -186,7 +186,7 @@ func TestResolve(t *testing.T) { params := []pipelinev1.Param{} if tc.paramSet { params = append(params, pipelinev1.Param{ - Name: urlParam, + Name: UrlParam, Value: *pipelinev1.NewStructuredValues(svr.URL), }) } @@ -253,12 +253,12 @@ func createRequest(params *params) *v1beta1.ResolutionRequest { Namespace: "foo", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: map[string]string{ - resolutioncommon.LabelKeyResolverType: LabelValueHttpResolverType, + common.LabelKeyResolverType: LabelValueHttpResolverType, }, }, Spec: v1beta1.ResolutionRequestSpec{ Params: []pipelinev1.Param{{ - Name: urlParam, + Name: UrlParam, Value: *pipelinev1.NewStructuredValues(params.url), }}, }, @@ -269,7 +269,7 @@ func createRequest(params *params) *v1beta1.ResolutionRequest { s = "" } rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: httpBasicAuthSecret, + Name: HttpBasicAuthSecret, Value: *pipelinev1.NewStructuredValues(s), }) } @@ -280,14 +280,14 @@ func createRequest(params *params) *v1beta1.ResolutionRequest { s = "" } rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: httpBasicAuthUsername, + Name: HttpBasicAuthUsername, Value: *pipelinev1.NewStructuredValues(s), }) } if params.authSecretKey != "" { rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{ - Name: httpBasicAuthSecretKey, + Name: HttpBasicAuthSecretKey, Value: *pipelinev1.NewStructuredValues(params.authSecretKey), }) } @@ -309,12 +309,12 @@ func TestResolverReconcileBasicAuth(t *testing.T) { { name: "good/URL Resolution", taskContent: sampleTask, - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), }, { name: "good/URL Resolution with custom basic auth, and custom secret key", taskContent: sampleTask, - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), params: ¶ms{ authSecret: "auth-secret", authUsername: "auth", @@ -325,7 +325,7 @@ func TestResolverReconcileBasicAuth(t *testing.T) { { name: "good/URL Resolution with custom basic auth no custom secret key", taskContent: sampleTask, - expectedStatus: internal.CreateResolutionRequestStatusWithData([]byte(sampleTask)), + expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte(sampleTask)), params: ¶ms{ authSecret: "auth-secret", authUsername: "auth", @@ -510,7 +510,7 @@ func toParams(m map[string]string) []pipelinev1.Param { func contextWithConfig(timeout string) context.Context { config := map[string]string{ - timeoutKey: timeout, + TimeoutKey: timeout, } return framework.InjectResolverConfigToContext(context.Background(), config) } diff --git a/pkg/resolution/resolver/hub/resolver.go b/pkg/resolution/resolver/hub/resolver.go index e94aa390fa5..a4f11726f99 100644 --- a/pkg/resolution/resolver/hub/resolver.go +++ b/pkg/resolution/resolver/hub/resolver.go @@ -27,7 +27,7 @@ import ( goversion "github.com/hashicorp/go-version" resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" ) @@ -77,7 +77,11 @@ func (r *Resolver) GetSelector(context.Context) map[string]string { // ValidateParams ensures parameters from a request are as expected. func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { - if r.isDisabled(ctx) { + return ValidateParams(ctx, params, r.TektonHubURL) +} + +func ValidateParams(ctx context.Context, params []pipelinev1.Param, tektonHubUrl string) error { + if isDisabled(ctx) { return errors.New(disabledError) } @@ -85,7 +89,7 @@ func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param if err != nil { return fmt.Errorf("failed to populate default params: %w", err) } - if err := r.validateParams(ctx, paramsMap); err != nil { + if err := validateParams(ctx, paramsMap, tektonHubUrl); err != nil { return fmt.Errorf("failed to validate params: %w", err) } @@ -110,7 +114,11 @@ type artifactHubResponse struct { // Resolve uses the given params to resolve the requested file or resource. func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (framework.ResolvedResource, error) { - if r.isDisabled(ctx) { + return Resolve(ctx, params, r.TektonHubURL, r.ArtifactHubURL) +} + +func Resolve(ctx context.Context, params []pipelinev1.Param, tektonHubURL, artifactHubURL string) (framework.ResolvedResource, error) { + if isDisabled(ctx) { return nil, errors.New(disabledError) } @@ -118,12 +126,12 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram if err != nil { return nil, fmt.Errorf("failed to populate default params: %w", err) } - if err := r.validateParams(ctx, paramsMap); err != nil { + if err := validateParams(ctx, paramsMap, tektonHubURL); err != nil { return nil, fmt.Errorf("failed to validate params: %w", err) } if constraint, err := goversion.NewConstraint(paramsMap[ParamVersion]); err == nil { - chosen, err := r.resolveVersionConstraint(ctx, paramsMap, constraint) + chosen, err := resolveVersionConstraint(ctx, paramsMap, constraint, artifactHubURL, tektonHubURL) if err != nil { return nil, err } @@ -139,7 +147,7 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram // call hub API switch paramsMap[ParamType] { case ArtifactHubType: - url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.ArtifactHubURL, ArtifactHubYamlEndpoint), + url := fmt.Sprintf(fmt.Sprintf("%s/%s", artifactHubURL, ArtifactHubYamlEndpoint), paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion]) resp := artifactHubResponse{} if err := fetchHubResource(ctx, url, &resp); err != nil { @@ -150,7 +158,7 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram Content: []byte(resp.Data.YAML), }, nil case TektonHubType: - url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.TektonHubURL, TektonHubYamlEndpoint), + url := fmt.Sprintf(fmt.Sprintf("%s/%s", tektonHubURL, TektonHubYamlEndpoint), paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion]) resp := tektonHubResponse{} if err := fetchHubResource(ctx, url, &resp); err != nil { @@ -198,7 +206,7 @@ func (rr *ResolvedHubResource) RefSource() *pipelinev1.RefSource { } } -func (r *Resolver) isDisabled(ctx context.Context) bool { +func isDisabled(ctx context.Context) bool { cfg := resolverconfig.FromContextOrDefaults(ctx) return !cfg.FeatureFlags.EnableHubResolver } @@ -288,10 +296,92 @@ type tektonHubListResult struct { Data tektonHubListDataResult `json:"data"` } -func (r *Resolver) resolveVersionConstraint(ctx context.Context, paramsMap map[string]string, constraint goversion.Constraints) (*goversion.Version, error) { +// the Artifact Hub follows the semVer (i.e. ..0) +// the Tekton Hub follows the simplified semVer (i.e. .) +// for resolution request with "artifact" type, we append ".0" suffix if the input version is simplified semVer +// for resolution request with "tekton" type, we only use . part of the input if it is semVer +func resolveVersion(version, hubType string) (string, error) { + semVer := strings.Split(version, ".") + resVer := version + + if hubType == ArtifactHubType && len(semVer) == 2 { + resVer = version + ".0" + } else if hubType == TektonHubType && len(semVer) > 2 { + resVer = strings.Join(semVer[0:2], ".") + } + + return resVer, nil +} + +func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { + conf := framework.GetResolverConfigFromContext(ctx) + paramsMap := make(map[string]string) + for _, p := range params { + paramsMap[p.Name] = p.Value.StringVal + } + + // type + if _, ok := paramsMap[ParamType]; !ok { + if typeString, ok := conf[ConfigType]; ok { + paramsMap[ParamType] = typeString + } else { + return nil, errors.New("default type was not set during installation of the hub resolver") + } + } + + // kind + if _, ok := paramsMap[ParamKind]; !ok { + if kindString, ok := conf[ConfigKind]; ok { + paramsMap[ParamKind] = kindString + } else { + return nil, errors.New("default resource kind was not set during installation of the hub resolver") + } + } + + // catalog + resCatName, err := resolveCatalogName(paramsMap, conf) + if err != nil { + return nil, err + } + paramsMap[ParamCatalog] = resCatName + + return paramsMap, nil +} + +func validateParams(ctx context.Context, paramsMap map[string]string, tektonHubURL string) error { + var missingParams []string + if _, ok := paramsMap[ParamName]; !ok { + missingParams = append(missingParams, ParamName) + } + if _, ok := paramsMap[ParamVersion]; !ok { + missingParams = append(missingParams, ParamVersion) + } + if kind, ok := paramsMap[ParamKind]; ok { + if kind != "task" && kind != "pipeline" { + return errors.New("kind param must be task or pipeline") + } + } + if hubType, ok := paramsMap[ParamType]; ok { + if hubType != ArtifactHubType && hubType != TektonHubType { + return fmt.Errorf("type param must be %s or %s", ArtifactHubType, TektonHubType) + } + + if hubType == TektonHubType && tektonHubURL == "" { + return errors.New("please configure TEKTON_HUB_API env variable to use tekton type") + } + } + + if len(missingParams) > 0 { + return fmt.Errorf("missing required hub resolver params: %s", strings.Join(missingParams, ", ")) + } + + return nil +} + +func resolveVersionConstraint(ctx context.Context, paramsMap map[string]string, constraint goversion.Constraints, artifactHubURL, tektonHubURL string) (*goversion.Version, error) { var ret *goversion.Version if paramsMap[ParamType] == ArtifactHubType { - allVersionsURL := fmt.Sprintf("%s/%s", r.ArtifactHubURL, fmt.Sprintf( + allVersionsURL := fmt.Sprintf("%s/%s", artifactHubURL, fmt.Sprintf( ArtifactHubListTasksEndpoint, paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName])) resp := artifactHubListResult{} @@ -318,7 +408,7 @@ func (r *Resolver) resolveVersionConstraint(ctx context.Context, paramsMap map[s } } } else if paramsMap[ParamType] == TektonHubType { - allVersionsURL := fmt.Sprintf("%s/%s", r.TektonHubURL, + allVersionsURL := fmt.Sprintf("%s/%s", tektonHubURL, fmt.Sprintf(TektonHubListTasksEndpoint, paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName])) resp := tektonHubListResult{} @@ -347,85 +437,3 @@ func (r *Resolver) resolveVersionConstraint(ctx context.Context, paramsMap map[s } return ret, nil } - -// the Artifact Hub follows the semVer (i.e. ..0) -// the Tekton Hub follows the simplified semVer (i.e. .) -// for resolution request with "artifact" type, we append ".0" suffix if the input version is simplified semVer -// for resolution request with "tekton" type, we only use . part of the input if it is semVer -func resolveVersion(version, hubType string) (string, error) { - semVer := strings.Split(version, ".") - resVer := version - - if hubType == ArtifactHubType && len(semVer) == 2 { - resVer = version + ".0" - } else if hubType == TektonHubType && len(semVer) > 2 { - resVer = strings.Join(semVer[0:2], ".") - } - - return resVer, nil -} - -func (r *Resolver) validateParams(ctx context.Context, paramsMap map[string]string) error { - var missingParams []string - if _, ok := paramsMap[ParamName]; !ok { - missingParams = append(missingParams, ParamName) - } - if _, ok := paramsMap[ParamVersion]; !ok { - missingParams = append(missingParams, ParamVersion) - } - if kind, ok := paramsMap[ParamKind]; ok { - if kind != "task" && kind != "pipeline" { - return errors.New("kind param must be task or pipeline") - } - } - if hubType, ok := paramsMap[ParamType]; ok { - if hubType != ArtifactHubType && hubType != TektonHubType { - return fmt.Errorf("type param must be %s or %s", ArtifactHubType, TektonHubType) - } - - if hubType == TektonHubType && r.TektonHubURL == "" { - return errors.New("please configure TEKTON_HUB_API env variable to use tekton type") - } - } - - if len(missingParams) > 0 { - return fmt.Errorf("missing required hub resolver params: %s", strings.Join(missingParams, ", ")) - } - - return nil -} - -func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[string]string, error) { - conf := framework.GetResolverConfigFromContext(ctx) - paramsMap := make(map[string]string) - for _, p := range params { - paramsMap[p.Name] = p.Value.StringVal - } - - // type - if _, ok := paramsMap[ParamType]; !ok { - if typeString, ok := conf[ConfigType]; ok { - paramsMap[ParamType] = typeString - } else { - return nil, errors.New("default type was not set during installation of the hub resolver") - } - } - - // kind - if _, ok := paramsMap[ParamKind]; !ok { - if kindString, ok := conf[ConfigKind]; ok { - paramsMap[ParamKind] = kindString - } else { - return nil, errors.New("default resource kind was not set during installation of the hub resolver") - } - } - - // catalog - resCatName, err := resolveCatalogName(paramsMap, conf) - if err != nil { - return nil, err - } - paramsMap[ParamCatalog] = resCatName - - return paramsMap, nil -} diff --git a/pkg/resolution/resolver/hub/resolver_test.go b/pkg/resolution/resolver/hub/resolver_test.go index 474838c1a7f..918d3a8d521 100644 --- a/pkg/resolution/resolver/hub/resolver_test.go +++ b/pkg/resolution/resolver/hub/resolver_test.go @@ -29,7 +29,7 @@ import ( "github.com/google/go-cmp/cmp" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" frtesting "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework/testing" "github.com/tektoncd/pipeline/test/diff" @@ -38,7 +38,7 @@ import ( func TestGetSelector(t *testing.T) { resolver := Resolver{} sel := resolver.GetSelector(context.Background()) - if typ, has := sel[resolutioncommon.LabelKeyResolverType]; !has { + if typ, has := sel[common.LabelKeyResolverType]; !has { t.Fatalf("unexpected selector: %v", sel) } else if typ != LabelValueHubResolverType { t.Fatalf("unexpected type: %q", typ) diff --git a/pkg/resolution/resource/crd_resource.go b/pkg/resolution/resource/crd_resource.go index 90fd7653303..39e41692075 100644 --- a/pkg/resolution/resource/crd_resource.go +++ b/pkg/resolution/resource/crd_resource.go @@ -22,11 +22,11 @@ import ( "errors" "fmt" - pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" rrclient "github.com/tektoncd/pipeline/pkg/client/resolution/clientset/versioned" rrlisters "github.com/tektoncd/pipeline/pkg/client/resolution/listers/resolution/v1beta1" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" @@ -63,7 +63,7 @@ func (r *CRDRequester) Submit(ctx context.Context, resolver ResolverName, req Re !apierrors.IsAlreadyExists(err) { return nil, err } - return nil, resolutioncommon.ErrRequestInProgress + return nil, common.ErrRequestInProgress } if rr.Status.GetCondition(apis.ConditionSucceeded).IsUnknown() { @@ -72,53 +72,59 @@ func (r *CRDRequester) Submit(ctx context.Context, resolver ResolverName, req Re // that it doesn't get deleted until the caller is done // with it. Use appendOwnerReference and then submit // update to ResolutionRequest. - return nil, resolutioncommon.ErrRequestInProgress + return nil, common.ErrRequestInProgress } if rr.Status.GetCondition(apis.ConditionSucceeded).IsTrue() { - return crdIntoResource(rr), nil + return CrdIntoResource(rr), nil } message := rr.Status.GetCondition(apis.ConditionSucceeded).GetMessage() - err := resolutioncommon.NewError(resolutioncommon.ReasonResolutionFailed, errors.New(message)) + err := common.NewError(common.ReasonResolutionFailed, errors.New(message)) return nil, err } func (r *CRDRequester) createResolutionRequest(ctx context.Context, resolver ResolverName, req Request) error { + var owner metav1.OwnerReference + if ownedReq, ok := req.(OwnedRequest); ok { + owner = ownedReq.OwnerRef() + } + rr := CreateResolutionRequest(ctx, resolver, req.Name(), req.Namespace(), req.Params(), owner) + _, err := r.clientset.ResolutionV1beta1().ResolutionRequests(rr.Namespace).Create(ctx, rr, metav1.CreateOptions{}) + return err +} + +func CreateResolutionRequest(ctx context.Context, resolver common.ResolverName, name, namespace string, params []v1.Param, ownerRef metav1.OwnerReference) *v1beta1.ResolutionRequest { rr := &v1beta1.ResolutionRequest{ TypeMeta: metav1.TypeMeta{ APIVersion: "resolution.tekton.dev/v1beta1", Kind: "ResolutionRequest", }, ObjectMeta: metav1.ObjectMeta{ - Name: req.Name(), - Namespace: req.Namespace(), + Name: name, + Namespace: namespace, Labels: map[string]string{ - resolutioncommon.LabelKeyResolverType: string(resolver), + common.LabelKeyResolverType: string(resolver), }, }, Spec: v1beta1.ResolutionRequestSpec{ - Params: req.Params(), + Params: params, }, } - appendOwnerReference(rr, req) - _, err := r.clientset.ResolutionV1beta1().ResolutionRequests(rr.Namespace).Create(ctx, rr, metav1.CreateOptions{}) - return err + appendOwnerReference(rr, ownerRef) + return rr } -func appendOwnerReference(rr *v1beta1.ResolutionRequest, req Request) { - if ownedReq, ok := req.(OwnedRequest); ok { - newOwnerRef := ownedReq.OwnerRef() - isOwner := false - for _, ref := range rr.ObjectMeta.OwnerReferences { - if ownerRefsAreEqual(ref, newOwnerRef) { - isOwner = true - } - } - if !isOwner { - rr.ObjectMeta.OwnerReferences = append(rr.ObjectMeta.OwnerReferences, newOwnerRef) +func appendOwnerReference(rr *v1beta1.ResolutionRequest, ownerRef metav1.OwnerReference) { + isOwner := false + for _, ref := range rr.ObjectMeta.OwnerReferences { + if ownerRefsAreEqual(ref, ownerRef) { + isOwner = true } } + if !isOwner { + rr.ObjectMeta.OwnerReferences = append(rr.ObjectMeta.OwnerReferences, ownerRef) + } } func ownerRefsAreEqual(a, b metav1.OwnerReference) bool { @@ -131,21 +137,21 @@ func ownerRefsAreEqual(a, b metav1.OwnerReference) bool { return a.APIVersion == b.APIVersion && a.Kind == b.Kind && a.Name == b.Name && a.UID == b.UID } -// readOnlyResolutionRequest is an opaque wrapper around ResolutionRequest +// ReadOnlyResolutionRequest is an opaque wrapper around ResolutionRequest // that provides the methods needed to read data from it using the // Resource interface without exposing the underlying API // object. -type readOnlyResolutionRequest struct { +type ReadOnlyResolutionRequest struct { req *v1beta1.ResolutionRequest } -var _ ResolvedResource = readOnlyResolutionRequest{} +var _ common.ResolvedResource = ReadOnlyResolutionRequest{} -func crdIntoResource(rr *v1beta1.ResolutionRequest) readOnlyResolutionRequest { - return readOnlyResolutionRequest{req: rr} +func CrdIntoResource(rr *v1beta1.ResolutionRequest) ReadOnlyResolutionRequest { + return ReadOnlyResolutionRequest{req: rr} } -func (r readOnlyResolutionRequest) Annotations() map[string]string { +func (r ReadOnlyResolutionRequest) Annotations() map[string]string { status := r.req.GetStatus() if status != nil && status.Annotations != nil { annotationsCopy := map[string]string{} @@ -157,7 +163,7 @@ func (r readOnlyResolutionRequest) Annotations() map[string]string { return nil } -func (r readOnlyResolutionRequest) Data() ([]byte, error) { +func (r ReadOnlyResolutionRequest) Data() ([]byte, error) { encodedData := r.req.Status.ResolutionRequestStatusFields.Data decodedBytes, err := base64.StdEncoding.Strict().DecodeString(encodedData) if err != nil { @@ -166,6 +172,6 @@ func (r readOnlyResolutionRequest) Data() ([]byte, error) { return decodedBytes, nil } -func (r readOnlyResolutionRequest) RefSource() *pipelinev1.RefSource { +func (r ReadOnlyResolutionRequest) RefSource() *v1.RefSource { return r.req.Status.RefSource } diff --git a/pkg/resolution/resource/crd_resource_test.go b/pkg/resolution/resource/crd_resource_test.go index da5a06fac38..c63a3564977 100644 --- a/pkg/resolution/resource/crd_resource_test.go +++ b/pkg/resolution/resource/crd_resource_test.go @@ -25,10 +25,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing" - resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resource" "github.com/tektoncd/pipeline/test" "github.com/tektoncd/pipeline/test/diff" + resolution "github.com/tektoncd/pipeline/test/resolution" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/logging" _ "knative.dev/pkg/system/testing" // Setup system.Namespace() @@ -144,7 +145,7 @@ conditions: testCases := []struct { name string - inputRequest *test.RawRequest + inputRequest *resolution.RawRequest inputResolutionRequest *v1beta1.ResolutionRequest expectedResolutionRequest *v1beta1.ResolutionRequest expectedResolvedResource *v1beta1.ResolutionRequest @@ -156,7 +157,7 @@ conditions: inputResolutionRequest: nil, expectedResolutionRequest: createdRR.DeepCopy(), expectedResolvedResource: nil, - expectedErr: resolutioncommon.ErrRequestInProgress, + expectedErr: common.ErrRequestInProgress, }, { name: "resolution request exist and status is unknown", @@ -164,7 +165,7 @@ conditions: inputResolutionRequest: unknownRR.DeepCopy(), expectedResolutionRequest: nil, expectedResolvedResource: nil, - expectedErr: resolutioncommon.ErrRequestInProgress, + expectedErr: common.ErrRequestInProgress, }, { name: "resolution request exist and status is succeeded", @@ -188,7 +189,7 @@ conditions: inputResolutionRequest: failedRR.DeepCopy(), expectedResolutionRequest: nil, expectedResolvedResource: nil, - expectedErr: resolutioncommon.NewError(resolutioncommon.ReasonResolutionFailed, errors.New("error message")), + expectedErr: common.NewError(common.ReasonResolutionFailed, errors.New("error message")), }, } @@ -204,7 +205,7 @@ conditions: ctx := testAssets.Ctx clients := testAssets.Clients - resolver := resolutioncommon.ResolverName("git") + resolver := common.ResolverName("git") crdRequester := resource.NewCRDRequester(clients.ResolutionRequests, testAssets.Informers.ResolutionRequest.Lister()) requestWithOwner := &ownerRequest{ Request: tc.inputRequest.Request(), @@ -235,7 +236,7 @@ conditions: if err != nil { t.Errorf("unexpected error decoding expected resource data: %v", err) } - expectedResolvedResource := test.NewResolvedResource(data, rr.Status.Annotations, rr.Status.RefSource, nil) + expectedResolvedResource := resolution.NewResolvedResource(data, rr.Status.Annotations, rr.Status.RefSource, nil) assertResolvedResourceEqual(t, expectedResolvedResource, resolvedResource) } @@ -255,7 +256,7 @@ conditions: } type ownerRequest struct { - resolutioncommon.Request + common.Request ownerRef metav1.OwnerReference } @@ -263,9 +264,9 @@ func (r *ownerRequest) OwnerRef() metav1.OwnerReference { return r.ownerRef } -func mustParseRawRequest(t *testing.T, yamlStr string) *test.RawRequest { +func mustParseRawRequest(t *testing.T, yamlStr string) *resolution.RawRequest { t.Helper() - output := &test.RawRequest{} + output := &resolution.RawRequest{} if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil { t.Errorf("parsing raw request %s: %v", yamlStr, err) } @@ -299,7 +300,7 @@ func mustParseResolutionRequestStatus(t *testing.T, yamlStr string) *v1beta1.Res return output } -func assertResolvedResourceEqual(t *testing.T, expected, actual resolutioncommon.ResolvedResource) { +func assertResolvedResourceEqual(t *testing.T, expected, actual common.ResolvedResource) { t.Helper() expectedBytes, err := expected.Data() if err != nil { diff --git a/pkg/resolution/resource/name.go b/pkg/resolution/resource/name.go index 051eabc89d0..37ec04d46ea 100644 --- a/pkg/resolution/resource/name.go +++ b/pkg/resolution/resource/name.go @@ -23,24 +23,58 @@ import ( "sort" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1" + "knative.dev/pkg/kmeta" ) +// GenerateDeterministicName makes a best-effort attempt to create a +// unique but reproducible name for use in a Request. The returned value +// will have the format {prefix}-{hash} where {prefix} is +// given and {hash} is nameHasher(base) + nameHasher(param1) + +// nameHasher(param2) + ... +func GenerateDeterministicName(prefix, base string, params v1.Params) (string, error) { + return GenerateDeterministicNameFromSpec(prefix, base, &v1beta1.ResolutionRequestSpec{Params: params}) +} + +func GetNameAndNamespace(resolverName string, owner kmeta.OwnerRefable, name string, namespace string, params v1.Params) (string, string, error) { + if name == "" { + name = owner.GetObjectMeta().GetName() + namespace = owner.GetObjectMeta().GetNamespace() + } + if namespace == "" { + namespace = "default" + } + // Generating a deterministic name for the resource request + // prevents multiple requests being issued for the same + // pipelinerun's pipelineRef or taskrun's taskRef. + remoteResourceBaseName := namespace + "/" + name + name, err := GenerateDeterministicNameFromSpec(resolverName, remoteResourceBaseName, &v1beta1.ResolutionRequestSpec{Params: params}) + if err != nil { + return "", "", fmt.Errorf("error generating name for taskrun %s/%s: %w", namespace, name, err) + } + return name, namespace, nil +} + // nameHasher returns the hash.Hash to use when generating names. func nameHasher() hash.Hash { return fnv.New128a() } -// GenerateDeterministicName makes a best-effort attempt to create a +// GenerateDeterministicNameFromSpec makes a best-effort attempt to create a // unique but reproducible name for use in a Request. The returned value // will have the format {prefix}-{hash} where {prefix} is // given and {hash} is nameHasher(base) + nameHasher(param1) + // nameHasher(param2) + ... -func GenerateDeterministicName(prefix, base string, params v1.Params) (string, error) { +func GenerateDeterministicNameFromSpec(prefix, base string, resolutionSpec *v1beta1.ResolutionRequestSpec) (string, error) { hasher := nameHasher() if _, err := hasher.Write([]byte(base)); err != nil { return "", err } + if resolutionSpec == nil { + return fmt.Sprintf("%s-%x", prefix, hasher.Sum(nil)), nil + } + params := resolutionSpec.Params sortedParams := make(v1.Params, len(params)) for i := range params { sortedParams[i] = *params[i].DeepCopy() diff --git a/pkg/resolution/resource/request.go b/pkg/resolution/resource/request.go index 9e0f3e194e7..792267ad36f 100644 --- a/pkg/resolution/resource/request.go +++ b/pkg/resolution/resource/request.go @@ -33,8 +33,6 @@ func NewRequest(name, namespace string, params v1.Params) Request { return &BasicRequest{name, namespace, params} } -var _ Request = &BasicRequest{} - // Name returns the name attached to the request func (req *BasicRequest) Name() string { return req.name diff --git a/test/remoteresolution/resolution.go b/test/remoteresolution/resolution.go new file mode 100644 index 00000000000..daefaed94ea --- /dev/null +++ b/test/remoteresolution/resolution.go @@ -0,0 +1,166 @@ +/* +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 test + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/google/go-cmp/cmp" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + resource "github.com/tektoncd/pipeline/pkg/remoteresolution/resource" + resolution "github.com/tektoncd/pipeline/pkg/resolution/common" + "github.com/tektoncd/pipeline/test/diff" +) + +var _ resource.Requester = &Requester{} +var _ resolution.ResolvedResource = &ResolvedResource{} + +// NewResolvedResource creates a mock resolved resource that is +// populated with the given data and annotations or returns the given +// error from its Data() method. +func NewResolvedResource(data []byte, annotations map[string]string, source *pipelinev1.RefSource, dataErr error) *ResolvedResource { + return &ResolvedResource{ + ResolvedData: data, + ResolvedAnnotations: annotations, + ResolvedRefSource: source, + DataErr: dataErr, + } +} + +// NewRequester creates a mock requester that resolves to the given +// resource or returns the given error on Submit(). +func NewRequester(resource resolution.ResolvedResource, err error, resolverPayload resource.ResolverPayload) *Requester { + return &Requester{ + ResolvedResource: resource, + SubmitErr: err, + ResolverPayload: resolverPayload, + } +} + +// Requester implements resolution.Requester and makes it easier +// to mock the outcome of a remote pipelineRef or taskRef resolution. +type Requester struct { + // The resolved resource object to return when a request is + // submitted. + ResolvedResource resolution.ResolvedResource + // An error to return when a request is submitted. + SubmitErr error + // ResolverPayload that should match that of the request in order to return the resolved resource + ResolverPayload resource.ResolverPayload +} + +// Submit implements resolution.Requester, accepting the name of a +// resolver and a request for a specific remote file, and then returns +// whatever mock data was provided on initialization. +func (r *Requester) Submit(ctx context.Context, resolverName resolution.ResolverName, req resource.Request) (resolution.ResolvedResource, error) { + if (r.ResolverPayload == resource.ResolverPayload{} || r.ResolverPayload.ResolutionSpec == nil || len(r.ResolverPayload.ResolutionSpec.Params) == 0) { + return r.ResolvedResource, r.SubmitErr + } + + reqParams := make(map[string]pipelinev1.ParamValue) + for _, p := range req.ResolverPayload().ResolutionSpec.Params { + reqParams[p.Name] = p.Value + } + + var wrongParams []string + for _, p := range r.ResolverPayload.ResolutionSpec.Params { + if reqValue, ok := reqParams[p.Name]; !ok { + wrongParams = append(wrongParams, fmt.Sprintf("expected %s param to be %#v, but was %#v", p.Name, p.Value, reqValue)) + } else if d := cmp.Diff(p.Value, reqValue); d != "" { + wrongParams = append(wrongParams, fmt.Sprintf("%s param did not match: %s", p.Name, diff.PrintWantGot(d))) + } + } + if len(wrongParams) > 0 { + return nil, errors.New(strings.Join(wrongParams, "; ")) + } + + return r.ResolvedResource, r.SubmitErr +} + +// ResolvedResource implements resolution.ResolvedResource and makes +// it easier to mock the resolved content of a fetched pipeline or task. +type ResolvedResource struct { + // The resolved bytes to return when resolution is complete. + ResolvedData []byte + // An error to return instead of the resolved bytes after + // resolution completes. + DataErr error + // Annotations to return when resolution is complete. + ResolvedAnnotations map[string]string + // ResolvedRefSource to return the source reference of the remote data + ResolvedRefSource *pipelinev1.RefSource +} + +// Data implements resolution.ResolvedResource and returns the mock +// data and/or error given to it on initialization. +func (r *ResolvedResource) Data() ([]byte, error) { + return r.ResolvedData, r.DataErr +} + +// Annotations implements resolution.ResolvedResource and returns +// the mock annotations given to it on initialization. +func (r *ResolvedResource) Annotations() map[string]string { + return r.ResolvedAnnotations +} + +// RefSource is the source reference of the remote data that records where the remote +// file came from including the url, digest and the entrypoint. +func (r *ResolvedResource) RefSource() *pipelinev1.RefSource { + return r.ResolvedRefSource +} + +// RawRequest stores the raw request data +type RawRequest struct { + ResolverPayload resource.ResolverPayload +} + +// Request returns a Request interface based on the RawRequest. +func (r *RawRequest) Request() resource.Request { + if r == nil { + r = &RawRequest{} + } + return &Request{ + RawRequest: *r, + } +} + +// Request implements resolution.Request and makes it easier to mock input for submit +// Using inline structs is to avoid conflicts between field names and method names. +type Request struct { + RawRequest +} + +var _ resource.Request = &Request{} + +// NewRequest creates a mock request that is populated with the given name namespace and params +func NewRequest(resolverPayload resource.ResolverPayload) *Request { + return &Request{ + RawRequest: RawRequest{ + ResolverPayload: resolverPayload, + }, + } +} + +// Params implements resolution.Request and returns the mock params given to it on initialization. +func (r *Request) ResolverPayload() resource.ResolverPayload { + return r.RawRequest.ResolverPayload +} + +var _ resource.Request = &Request{} diff --git a/test/resolution.go b/test/resolution/resolution.go similarity index 91% rename from test/resolution.go rename to test/resolution/resolution.go index 514988427f2..b8b940658f9 100644 --- a/test/resolution.go +++ b/test/resolution/resolution.go @@ -24,16 +24,16 @@ import ( "github.com/google/go-cmp/cmp" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - resolution "github.com/tektoncd/pipeline/pkg/resolution/common" + common "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/test/diff" ) -var _ resolution.Requester = &Requester{} -var _ resolution.ResolvedResource = &ResolvedResource{} +var _ common.Requester = &Requester{} +var _ common.ResolvedResource = &ResolvedResource{} // NewRequester creates a mock requester that resolves to the given // resource or returns the given error on Submit(). -func NewRequester(resource resolution.ResolvedResource, err error) *Requester { +func NewRequester(resource common.ResolvedResource, err error) *Requester { return &Requester{ ResolvedResource: resource, SubmitErr: err, @@ -57,7 +57,7 @@ func NewResolvedResource(data []byte, annotations map[string]string, source *pip type Requester struct { // The resolved resource object to return when a request is // submitted. - ResolvedResource resolution.ResolvedResource + ResolvedResource common.ResolvedResource // An error to return when a request is submitted. SubmitErr error // Params that should match those on the request in order to return the resolved resource @@ -67,7 +67,7 @@ type Requester struct { // Submit implements resolution.Requester, accepting the name of a // resolver and a request for a specific remote file, and then returns // whatever mock data was provided on initialization. -func (r *Requester) Submit(ctx context.Context, resolverName resolution.ResolverName, req resolution.Request) (resolution.ResolvedResource, error) { +func (r *Requester) Submit(ctx context.Context, resolverName common.ResolverName, req common.Request) (common.ResolvedResource, error) { if len(r.Params) == 0 { return r.ResolvedResource, r.SubmitErr } @@ -134,7 +134,7 @@ type RawRequest struct { } // Request returns a Request interface based on the RawRequest. -func (r *RawRequest) Request() resolution.Request { +func (r *RawRequest) Request() common.Request { if r == nil { r = &RawRequest{} } @@ -149,7 +149,7 @@ type Request struct { RawRequest } -var _ resolution.Request = &Request{} +var _ common.Request = &Request{} // NewRequest creates a mock request that is populated with the given name namespace and params func NewRequest(name, namespace string, params []pipelinev1.Param) *Request { diff --git a/test/resolvers_test.go b/test/resolvers_test.go index 1979a1703c2..3829a84b647 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -37,7 +37,7 @@ import ( resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/reconciler/pipelinerun" - "github.com/tektoncd/pipeline/pkg/resolution/resolver/git" + gitresolution "github.com/tektoncd/pipeline/pkg/resolution/resolver/git" "github.com/tektoncd/pipeline/test/parse" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -484,24 +484,24 @@ func TestGitResolver_API(t *testing.T) { resovlerNS := resolverconfig.ResolversNamespace(systemNamespace) - originalConfigMap, err := c.KubeClient.CoreV1().ConfigMaps(resovlerNS).Get(ctx, git.ConfigMapName, metav1.GetOptions{}) + originalConfigMap, err := c.KubeClient.CoreV1().ConfigMaps(resovlerNS).Get(ctx, gitresolution.ConfigMapName, metav1.GetOptions{}) if err != nil { - t.Fatalf("Failed to get ConfigMap `%s`: %s", git.ConfigMapName, err) + t.Fatalf("Failed to get ConfigMap `%s`: %s", gitresolution.ConfigMapName, err) } originalConfigMapData := originalConfigMap.Data - t.Logf("Creating ConfigMap %s", git.ConfigMapName) + t.Logf("Creating ConfigMap %s", gitresolution.ConfigMapName) configMapData := map[string]string{ - git.ServerURLKey: fmt.Sprint("http://", net.JoinHostPort(giteaClusterHostname, "3000")), - git.SCMTypeKey: "gitea", - git.APISecretNameKey: tokenSecretName, - git.APISecretKeyKey: scmTokenSecretKey, - git.APISecretNamespaceKey: namespace, + gitresolution.ServerURLKey: fmt.Sprint("http://", net.JoinHostPort(giteaClusterHostname, "3000")), + gitresolution.SCMTypeKey: "gitea", + gitresolution.APISecretNameKey: tokenSecretName, + gitresolution.APISecretKeyKey: scmTokenSecretKey, + gitresolution.APISecretNamespaceKey: namespace, } - if err := updateConfigMap(ctx, c.KubeClient, resovlerNS, git.ConfigMapName, configMapData); err != nil { + if err := updateConfigMap(ctx, c.KubeClient, resovlerNS, gitresolution.ConfigMapName, configMapData); err != nil { t.Fatal(err) } - defer resetConfigMap(ctx, t, c, resovlerNS, git.ConfigMapName, originalConfigMapData) + defer resetConfigMap(ctx, t, c, resovlerNS, gitresolution.ConfigMapName, originalConfigMapData) trName := helpers.ObjectNameForTest(t) tr := parse.MustParseV1TaskRun(t, fmt.Sprintf(`