From e3683755d78c75de82d67dcfbc39ed0f812a69e0 Mon Sep 17 00:00:00 2001 From: Quan Zhang Date: Wed, 5 Oct 2022 15:33:48 -0400 Subject: [PATCH] [TEP-0115] Support Artifact Hub in Hub Resolver Part of [issues/667]. This commit adds support to resolve catalog resource from the [Artifact Hub] while keeping current functionality of fetching resources from Tekton Hub. - Change 1: The commit adds a new field `type` to the hub resolver indicating the type of the Hub to pull the resource from. The value can be set to `tekton` or `artifact`. By default, the resolver fetches resources from `https://artifacthub.io/` when setting `type` to `" artifact"`, and fetches resources from user's private instance of Tekton Hub when setting `type` to `"tekton"`. - Change 2: Prior to this change, the hub resolver only supports pulling resources from the Tekton Hub. This commit updates the default hub type to `artifact` since the [Artifact Hub][Artifact Hub] will be the main entrypoint for Tekton Catalogs in the future. - Change 3: Prior to this change, the default Tekton Hub URL is: `https://api.hub.tekton.dev`. This commit removes the default value of the Tekton Hub URL and enforces users to configure their own instance of Tekton Hub since the public instance `https://api.hub.tekton.dev` will be deprecated after the migration to Artifact Hub is done. /kind feature [Artifact Hub]: https://artifacthub.io/ [issues/667]: https://github.com/tektoncd/hub/issues/667 --- cmd/resolvers/main.go | 29 +- config/resolvers/hubresolver-config.yaml | 12 +- config/resolvers/resolvers-deployment.yaml | 4 +- docs/hub-resolver.md | 50 +++- examples/v1beta1/taskruns/hub-resolver.yaml | 32 +- pkg/resolution/resolver/hub/config.go | 18 +- pkg/resolution/resolver/hub/params.go | 14 +- pkg/resolution/resolver/hub/resolver.go | 177 +++++++++-- pkg/resolution/resolver/hub/resolver_test.go | 292 ++++++++++++++++--- test/resolvers_test.go | 2 +- 10 files changed, 529 insertions(+), 101 deletions(-) diff --git a/cmd/resolvers/main.go b/cmd/resolvers/main.go index f6163c6d6e3..641c407785e 100644 --- a/cmd/resolvers/main.go +++ b/cmd/resolvers/main.go @@ -33,21 +33,26 @@ import ( func main() { ctx := filteredinformerfactory.WithSelectors(signals.NewContext(), v1alpha1.ManagedByLabelKey) - - apiURL := os.Getenv("HUB_API") - hubURL := hub.DefaultHubURL - if apiURL == "" { - hubURL = hub.DefaultHubURL - } else { - if !strings.HasSuffix(apiURL, "/") { - apiURL += "/" - } - hubURL = apiURL + hub.YamlEndpoint - } + tektonHubURL := buildHubURL(os.Getenv("TEKTON_HUB_API"), "", hub.TektonHubYamlEndpoint) + artifactHubURL := buildHubURL(os.Getenv("ARTIFACT_HUB_API"), hub.DefaultArtifactHubURL, hub.ArtifactHubYamlEndpoint) sharedmain.MainWithContext(ctx, "controller", framework.NewController(ctx, &git.Resolver{}), - framework.NewController(ctx, &hub.Resolver{HubURL: hubURL}), + framework.NewController(ctx, &hub.Resolver{TektonHubURL: tektonHubURL, ArtifactHubURL: artifactHubURL}), framework.NewController(ctx, &bundle.Resolver{}), framework.NewController(ctx, &cluster.Resolver{})) } + +func buildHubURL(configAPI, defaultURL, yamlEndpoint string) string { + var hubURL string + if configAPI == "" { + hubURL = defaultURL + } else { + if !strings.HasSuffix(configAPI, "/") { + configAPI += "/" + } + hubURL = configAPI + yamlEndpoint + } + + return hubURL +} diff --git a/config/resolvers/hubresolver-config.yaml b/config/resolvers/hubresolver-config.yaml index d4fe0ebcb95..c0bd9306a98 100644 --- a/config/resolvers/hubresolver-config.yaml +++ b/config/resolvers/hubresolver-config.yaml @@ -22,7 +22,13 @@ metadata: app.kubernetes.io/instance: default app.kubernetes.io/part-of: tekton-pipelines data: - # the default catalog from where to pull the resource. - default-catalog: "Tekton" - # The default layer kind in the hub image. + # the default Tekton Hub catalog from where to pull the resource. + default-tekton-hub-catalog: "Tekton" + # the default Artifact Hub Task catalog from where to pull the resource. + default-artifact-hub-task-catalog: "tekton-catalog-tasks" + # the default Artifact Hub Pipeline catalog from where to pull the resource. + default-artifact-hub-pipeline-catalog: "tekton-catalog-pipelines" + # the default layer kind in the hub image. default-kind: "task" + # the default hub source to pull the resource from. + default-type: "artifact" diff --git a/config/resolvers/resolvers-deployment.yaml b/config/resolvers/resolvers-deployment.yaml index 1a9cb28ac8a..a24a1cc30fc 100644 --- a/config/resolvers/resolvers-deployment.yaml +++ b/config/resolvers/resolvers-deployment.yaml @@ -93,8 +93,8 @@ spec: - name: METRICS_DOMAIN value: tekton.dev/resolution # Override this env var to set a private hub api endpoint - - name: HUB_API - value: "https://api.hub.tekton.dev/" + - name: ARTIFACT_HUB_API + value: "https://artifacthub.io/" securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/docs/hub-resolver.md b/docs/hub-resolver.md index 45d8fd3a264..7f73e378758 100644 --- a/docs/hub-resolver.md +++ b/docs/hub-resolver.md @@ -6,10 +6,13 @@ Use resolver type `hub`. | Param Name | Description | Example Value | |------------------|-------------------------------------------------------------------------------|------------------------------------------------------------| -| `catalog` | The catalog from where to pull the resource (Optional) | Default: `Tekton` | -| `kind` | Either `task` or `pipeline` | `task` | +| `catalog` | The catalog from where to pull the resource (Optional) | Default: `tekton-catalog-tasks` (for `task` kind); `tekton-catalog-pipelines` (for `pipeline` kind) | +| `type` | The type of Hub from where to pull the resource (Optional). Either `artifact` or `tekton` | Default: `artifact` | +| `kind` | Either `task` or `pipeline` (Optional) | Default: `task` | | `name` | The name of the task or pipeline to fetch from the hub | `golang-build` | -| `version` | Version of task or pipeline to pull in from hub. Wrap the number in quotes! | `"0.5"` | +| `version` | Version of task or pipeline to pull in from hub. Wrap the number in quotes! | `"0.5.0"` | + +The Catalogs in the Artifact Hub follows the semVer (i.e.` ..0`) and the Catalogs in the Tekton Hub follows the simplified semVer (i.e. `.`). Both full and simplified semantic versioning will be accepted by the `version` parameter. The Hub Resolver will map the version to the format expected by the target Hub `type`. ## Requirements @@ -26,25 +29,42 @@ for the name, namespace and defaults that the resolver ships with. ### Options -| Option Name | Description | Example Values | -|-------------------|------------------------------------------------------|--------------------| -| `default-catalog` | The default catalog from where to pull the resource. | `tekton` | -| `default-kind` | The default object kind for references. | `task`, `pipeline` | +| Option Name | Description | Example Values | +|-------------------|------------------------------------------------------|------------------------| +| `default-catalog` | The default catalog from where to pull the resource. | `tekton-catalog-tasks` | +| `default-kind` | The default object kind for references. | `task`, `pipeline` | +| `default-type` | The default hub from where to pull the resource. | `artifact`, `tekton` | ### Configuring the Hub API endpoint -By default this resolver will hit the public hub api at https://hub.tekton.dev/ +The Hub Resolver supports to resolve resources from the [Artifact Hub](https://artifacthub.io/) and the [Tekton Hub](https://hub.tekton.dev/), +which can be configured by setting the `type` field of the resolver. + +*(Please note that the [Tekton Hub](https://hub.tekton.dev/) will be deprecated after [migration to the Artifact Hub](https://github.com/tektoncd/hub/issues/667) is done.)* + +When setting the `type` field to `artifact`, the resolver will hit the public hub api at https://artifacthub.io/ by default but you can configure your own (for example to use a private hub -instance) by setting the `HUB_API` environment variable in +instance) by setting the `ARTIFACT_HUB_API` environment variable in +[`../config/resolvers/resolvers-deployment.yaml`](../config/resolvers/resolvers-deployment.yaml). Example: + +```yaml +env +- name: ARTIFACT_HUB_API + value: "https://artifacthub.io/" +``` + +When setting the `type` field to `tekton`, you **must** configure your own instance of the Tekton Hub by setting the `TEKTON_HUB_API` environment variable in [`../config/resolvers/resolvers-deployment.yaml`](../config/resolvers/resolvers-deployment.yaml). Example: ```yaml env -- name: HUB_API - value: "https://api.hub.tekton.dev/" +- name: TEKTON_HUB_API + value: "https://api.private.hub.instance.dev" ``` +The Tekton Hub deployment guide can be found [here](https://github.com/tektoncd/hub/blob/main/docs/DEPLOYMENT.md). + ## Usage ### Task Resolution @@ -59,7 +79,9 @@ spec: resolver: hub params: - name: catalog # optional - value: Tekton + value: tekton-catalog-tasks + - name: type # optional + value: artifact - name: kind value: task - name: name @@ -80,7 +102,9 @@ spec: resolver: hub params: - name: catalog # optional - value: Tekton + value: tekton-catalog-pipelines + - name: type # optional + value: artifact - name: kind value: pipeline - name: name diff --git a/examples/v1beta1/taskruns/hub-resolver.yaml b/examples/v1beta1/taskruns/hub-resolver.yaml index 3286dfded94..509e3659933 100644 --- a/examples/v1beta1/taskruns/hub-resolver.yaml +++ b/examples/v1beta1/taskruns/hub-resolver.yaml @@ -1,7 +1,7 @@ apiVersion: tekton.dev/v1beta1 kind: TaskRun metadata: - generateName: hub-resolver + generateName: hub-resolver-simple-semver- spec: workspaces: - name: output @@ -17,11 +17,35 @@ spec: taskRef: resolver: hub params: - - name: catalog # optional - value: Tekton - - name: kind + - name: type #optional + value: artifact + - name: kind #optional value: task - name: name value: git-clone - name: version value: "0.6" +--- +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + generateName: hub-resolver-semver-required-fields-only- +spec: + workspaces: + - name: output + emptyDir: {} + podTemplate: + securityContext: + fsGroup: 65532 + params: + - name: url + value: https://github.com/tektoncd/pipeline.git + - name: revision + value: main + taskRef: + resolver: hub + params: + - name: name + value: git-clone + - name: version + value: "0.6.0" \ No newline at end of file diff --git a/pkg/resolution/resolver/hub/config.go b/pkg/resolution/resolver/hub/config.go index d2ee00307c6..747b9c4e1ec 100644 --- a/pkg/resolution/resolver/hub/config.go +++ b/pkg/resolution/resolver/hub/config.go @@ -16,10 +16,22 @@ limitations under the License. package hub -// ConfigCatalog is the configuration field name for controlling -// the catalog to fetch the remote resource from. -const ConfigCatalog = "default-catalog" +// ConfigTektonHubCatalog is the configuration field name for controlling +// the Tekton Hub catalog to fetch the remote resource from. +const ConfigTektonHubCatalog = "default-tekton-hub-catalog" + +// ConfigArtifactHubTaskCatalog is the configuration field name for controlling +// the Artifact Hub Task catalog to fetch the remote resource from. +const ConfigArtifactHubTaskCatalog = "default-artifact-hub-task-catalog" + +// ConfigArtifactHubPipelineCatalog is the configuration field name for controlling +// the Artifact Hub Pipeline catalog to fetch the remote resource from. +const ConfigArtifactHubPipelineCatalog = "default-artifact-hub-pipeline-catalog" // ConfigKind is the configuration field name for controlling // what the layer name in the hub image is. const ConfigKind = "default-kind" + +// ConfigType is the configuration field name for controlling +// the hub type to pull the resource from. +const ConfigType = "default-type" diff --git a/pkg/resolution/resolver/hub/params.go b/pkg/resolution/resolver/hub/params.go index 24e19c6ec2f..f9bfd711055 100644 --- a/pkg/resolution/resolver/hub/params.go +++ b/pkg/resolution/resolver/hub/params.go @@ -13,11 +13,14 @@ limitations under the License. package hub -// DefaultHubURL is de default url for the Tekton hub api -const DefaultHubURL = "https://api.hub.tekton.dev/v1/resource/%s/%s/%s/%s/yaml" +// DefaultArtifactHubURL is the default url for the Artifact hub api +const DefaultArtifactHubURL = "https://artifacthub.io/api/v1/packages/tekton-%s/%s/%s/%s" -// YamlEndpoint is the suffix for a private custom hub instance -const YamlEndpoint = "v1/resource/%s/%s/%s/%s/yaml" +// TektonHubYamlEndpoint is the suffix for a private custom Tekton hub instance +const TektonHubYamlEndpoint = "v1/resource/%s/%s/%s/%s/yaml" + +// ArtifactHubYamlEndpoint is the suffix for a private custom Artifact hub instance +const ArtifactHubYamlEndpoint = "api/v1/packages/tekton-%s/%s/%s/%s" // ParamName is the parameter defining what the layer name in the bundle // image is. @@ -34,3 +37,6 @@ const ParamVersion = "version" // ParamCatalog is the parameter defining what the catalog in the bundle // image is. const ParamCatalog = "catalog" + +// ParamType is the parameter defining what the hub type to pull the resource from. +const ParamType = "type" diff --git a/pkg/resolution/resolver/hub/resolver.go b/pkg/resolution/resolver/hub/resolver.go index 42eb830c235..a79fda5704a 100644 --- a/pkg/resolution/resolver/hub/resolver.go +++ b/pkg/resolution/resolver/hub/resolver.go @@ -15,11 +15,14 @@ package hub import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" + "strings" resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver" pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -32,13 +35,21 @@ const ( // 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" + disabledError = "cannot handle resolution request, enable-hub-resolver feature flag not true" ) // Resolver implements a framework.Resolver that can fetch files from OCI bundles. type Resolver struct { - // HubURL is the URL for hub resolver - HubURL string + // 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. @@ -83,15 +94,33 @@ func (r *Resolver) ValidateParams(ctx context.Context, params []pipelinev1beta1. return errors.New("kind param must be task or pipeline") } } + if hubType, ok := paramsMap[ParamType]; ok { + if hubType.StringVal != ArtifactHubType && hubType.StringVal != TektonHubType { + return fmt.Errorf(fmt.Sprintf("type param must be %s or %s", ArtifactHubType, TektonHubType)) + } + + if hubType.StringVal == TektonHubType && r.TektonHubURL == "" { + return fmt.Errorf("pleaes configure TEKTON_HUB_API env variable to use tekton type") + } + } + return nil } -type dataResponse struct { +type tektonHubDataResponse struct { YAML string `json:"yaml"` } -type hubResponse struct { - Data dataResponse `json:"data"` +type tektonHubResponse struct { + Data tektonHubDataResponse `json:"data"` +} + +type artifactHubDataResponse struct { + YAML string `json:"manifestRaw"` +} + +type artifactHubResponse struct { + Data artifactHubDataResponse `json:"data"` } // Resolve uses the given params to resolve the requested file or resource. @@ -107,14 +136,19 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1beta1.Param) paramsMap[p.Name] = p.Value.StringVal } - if _, ok := paramsMap[ParamCatalog]; !ok { - if catalogString, ok := conf[ConfigCatalog]; ok { - paramsMap[ParamCatalog] = catalogString + // type + if _, ok := paramsMap[ParamType]; !ok { + if typeString, ok := conf[ConfigType]; ok { + paramsMap[ParamType] = typeString } else { - return nil, fmt.Errorf("default catalog was not set during installation of the hub resolver") + return nil, fmt.Errorf("default type was not set during installation of the hub resolver") } } + if paramsMap[ParamType] != ArtifactHubType && paramsMap[ParamType] != TektonHubType { + return nil, fmt.Errorf("type param must be artifact or tekton") + } + // kind kind, ok := paramsMap[ParamKind] if !ok { if kindString, ok := conf[ConfigKind]; ok { @@ -126,36 +160,126 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1beta1.Param) if kind != "task" && kind != "pipeline" { return nil, fmt.Errorf("kind param must be task or pipeline") } - paramsMap[ParamKind] = kind - url := fmt.Sprintf(r.HubURL, paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion]) + + // catalog + resCatName, err := resolveCatalogName(paramsMap, conf) + if err != nil { + return nil, err + } + paramsMap[ParamCatalog] = resCatName + + // version + resVer, err := resolveVersion(paramsMap[ParamVersion], paramsMap[ParamType]) + if err != nil { + return nil, err + } + paramsMap[ParamVersion] = resVer + + // call hub API + switch paramsMap[ParamType] { + case ArtifactHubType: + url := fmt.Sprintf(r.ArtifactHubURL, paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion]) + resp := artifactHubResponse{} + if err := fetchHubResource(url, &resp); err != nil { + return nil, fmt.Errorf("fail to fetch Artifact Hub resource: %v", err) + } + return &ResolvedHubResource{ + Content: []byte(resp.Data.YAML), + }, nil + case TektonHubType: + url := fmt.Sprintf(r.TektonHubURL, paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion]) + resp := tektonHubResponse{} + if err := fetchHubResource(url, &resp); err != nil { + return nil, fmt.Errorf("fail to fetch Tekton Hub resource: %v", err) + } + return &ResolvedHubResource{ + Content: []byte(resp.Data.YAML), + }, nil + } + + return nil, fmt.Errorf("hub resolver type: %s is not supported", paramsMap[ParamType]) +} + +func fetchHubResource(apiEndpoint string, v interface{}) error { // #nosec G107 -- URL cannot be constant in this case. - resp, err := http.Get(url) + resp, err := http.Get(apiEndpoint) if err != nil { - return nil, fmt.Errorf("error requesting resource from hub: %w", err) + return fmt.Errorf("error requesting resource from Hub: %w", err) } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("requested resource '%s' not found on hub", url) + return fmt.Errorf("requested resource '%s' not found on hub", apiEndpoint) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("error reading response body: %w", err) + return fmt.Errorf("error reading response body: %w", err) } - hr := hubResponse{} - err = json.Unmarshal(body, &hr) + + err = json.Unmarshal(body, v) if err != nil { - return nil, fmt.Errorf("error unmarshalling json response: %w", err) + return fmt.Errorf("error unmarshalling json response: %w", err) + } + + return nil +} + +func resolveCatalogName(paramsMap, conf map[string]string) (string, error) { + var configTHCatalog, configAHTaskCatalog, configAHPipelineCatalog string + var ok bool + + if configTHCatalog, ok = conf[ConfigTektonHubCatalog]; !ok { + return "", fmt.Errorf("default Tekton Hub catalog was not set during installation of the hub resolver") + } + if configAHTaskCatalog, ok = conf[ConfigArtifactHubTaskCatalog]; !ok { + return "", fmt.Errorf("default Artifact Hub task catalog was not set during installation of the hub resolver") + } + if configAHPipelineCatalog, ok = conf[ConfigArtifactHubPipelineCatalog]; !ok { + return "", fmt.Errorf("default Artifact Hub pipeline catalog was not set during installation of the hub resolver") } - return &ResolvedHubResource{ - Content: []byte(hr.Data.YAML), - }, nil + if _, ok := paramsMap[ParamCatalog]; !ok { + switch paramsMap[ParamType] { + case ArtifactHubType: + switch paramsMap[ParamKind] { + case "task": + return configAHTaskCatalog, nil + case "pipeline": + return configAHPipelineCatalog, nil + default: + return "", fmt.Errorf("failed to resolve catalog name with kind: %s", paramsMap[ParamKind]) + } + case TektonHubType: + return configTHCatalog, nil + default: + return "", fmt.Errorf("failed to resolve catalog name with type: %s", paramsMap[ParamType]) + } + } + + return paramsMap[ParamCatalog], 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 } // ResolvedHubResource wraps the data we want to return to Pipelines type ResolvedHubResource struct { + URL string Content []byte } @@ -174,7 +298,16 @@ func (*ResolvedHubResource) Annotations() map[string]string { // Source is the source reference of the remote data that records where the remote // file came from including the url, digest and the entrypoint. func (rr *ResolvedHubResource) Source() *pipelinev1beta1.ConfigSource { - return nil + h := sha256.New() + h.Write(rr.Content) + sha256CheckSum := hex.EncodeToString(h.Sum(nil)) + + return &pipelinev1beta1.ConfigSource{ + URI: rr.URL, + Digest: map[string]string{ + "sha256": sha256CheckSum, + }, + } } func (r *Resolver) isDisabled(ctx context.Context) bool { diff --git a/pkg/resolution/resolver/hub/resolver_test.go b/pkg/resolution/resolver/hub/resolver_test.go index 6974ebc2ac7..fc1c0f4b6ad 100644 --- a/pkg/resolution/resolver/hub/resolver_test.go +++ b/pkg/resolution/resolver/hub/resolver_test.go @@ -26,6 +26,7 @@ import ( "github.com/google/go-cmp/cmp" pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" resolutioncommon "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" ) @@ -41,26 +42,56 @@ func TestGetSelector(t *testing.T) { } func TestValidateParams(t *testing.T) { - resolver := Resolver{} - - paramsWithTask := map[string]string{ - ParamKind: "task", - ParamName: "foo", - ParamVersion: "bar", - ParamCatalog: "baz", - } - if err := resolver.ValidateParams(resolverContext(), toParams(paramsWithTask)); err != nil { - t.Fatalf("unexpected error validating params: %v", err) + 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: fmt.Errorf("pleaes configure TEKTON_HUB_API env variable to use tekton type"), + }, } - paramsWithPipeline := map[string]string{ - ParamKind: "pipeline", - ParamName: "foo", - ParamVersion: "bar", - ParamCatalog: "baz", - } - if err := resolver.ValidateParams(resolverContext(), toParams(paramsWithPipeline)); err != nil { - t.Fatalf("unexpected error validating params: %v", err) + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + resolver := Resolver{} + params := map[string]string{ + ParamKind: tc.kind, + ParamName: tc.resourceName, + ParamVersion: tc.version, + ParamCatalog: tc.catalog, + ParamType: tc.hubType, + } + + err := resolver.ValidateParams(resolverContext(), toParams(params)) + if tc.expectedErr != nil { + if err == nil { + t.Fatalf("expected err '%v' but didn't get one", tc.expectedErr) + } + if d := cmp.Diff(tc.expectedErr.Error(), err.Error()); d != "" { + t.Fatalf("expected err '%v' but got '%v'", tc.expectedErr, err) + } + } else if err != nil { + t.Fatalf("unexpected error validating params: %v", err) + } + }) } } @@ -110,16 +141,167 @@ func TestValidateParamsMissing(t *testing.T) { } func TestValidateParamsConflictingKindName(t *testing.T) { - resolver := Resolver{} - params := map[string]string{ - ParamKind: "not-taskpipeline", - ParamName: "foo", - ParamVersion: "bar", - ParamCatalog: "baz", + 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", + }, } - err := resolver.ValidateParams(resolverContext(), toParams(params)) - if err == nil { - t.Fatalf("expected err due to conflicting kind param") + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resolver := Resolver{} + params := map[string]string{ + ParamKind: tc.kind, + ParamName: tc.name, + ParamVersion: tc.version, + ParamCatalog: tc.catalog, + ParamType: tc.hubType, + } + err := resolver.ValidateParams(resolverContext(), toParams(params)) + if err == nil { + t.Fatalf("expected err due to conflicting param") + } + }) + } +} + +func TestResolveVersion(t *testing.T) { + testCases := []struct { + name string + version string + hubType string + expectedVer string + expectedErr error + }{ + { + name: "semver to Tekton Hub", + version: "0.6.0", + hubType: TektonHubType, + expectedVer: "0.6", + }, + { + name: "simplified semver to Tekton Hub", + version: "0.6", + hubType: TektonHubType, + expectedVer: "0.6", + }, + { + name: "semver to Artifact Hub", + version: "0.6.0", + hubType: ArtifactHubType, + expectedVer: "0.6.0", + }, + { + name: "simplified semver to Artifact Hub", + version: "0.6", + hubType: ArtifactHubType, + expectedVer: "0.6.0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resVer, err := resolveVersion(tc.version, tc.hubType) + if tc.expectedErr != nil { + if err == nil { + t.Fatalf("expected err '%v' but didn't get one", tc.expectedErr) + } + if d := cmp.Diff(tc.expectedErr.Error(), err.Error()); d != "" { + t.Fatalf("expected err '%v' but got '%v'", tc.expectedErr, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error resolving, %v", err) + } else { + if d := cmp.Diff(tc.expectedVer, resVer); d != "" { + t.Fatalf("expected version '%v' but got '%v'", tc.expectedVer, resVer) + } + } + } + }) + } +} + +func TestResolveCatalogName(t *testing.T) { + testCases := []struct { + name string + inputCat string + kind string + hubType string + expectedCat string + }{ + { + name: "tekton type default catalog", + kind: "task", + hubType: "tekton", + expectedCat: "Tekton", + }, + { + name: "artifact type default task catalog", + kind: "task", + hubType: "artifact", + expectedCat: "tekton-catalog-tasks", + }, + { + name: "artifact type default pipeline catalog", + kind: "pipeline", + hubType: "artifact", + expectedCat: "tekton-catalog-pipelines", + }, + { + name: "custom catalog", + inputCat: "custom-catalog", + kind: "task", + hubType: "artifact", + expectedCat: "custom-catalog", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := map[string]string{ + ParamKind: tc.kind, + ParamType: tc.hubType, + } + if tc.inputCat != "" { + params[ParamCatalog] = tc.inputCat + } + + 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", + } + ctx := framework.InjectResolverConfigToContext(resolverContext(), config) + conf := framework.GetResolverConfigFromContext(ctx) + + resCatalog, err := resolveCatalogName(params, conf) + if err != nil { + t.Fatalf("unexpected error resolving, %v", err) + } else { + if d := cmp.Diff(tc.expectedCat, resCatalog); d != "" { + t.Fatalf("expected catalog name '%v' but got '%v'", tc.expectedCat, resCatalog) + } + } + }) } } @@ -151,25 +333,38 @@ func TestResolve(t *testing.T) { imageName string version string catalog string + hubType string input string expectedRes []byte expectedErr error }{ { - name: "valid response from hub", + name: "valid response from Tekton Hub", kind: "task", imageName: "foo", version: "baz", - catalog: "tekton", + 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", + catalog: "Tekton", + hubType: TektonHubType, input: `{"name":"not-found","id":"aaaaaaaa","message":"resource not found","temporary":false,"timeout":false,"fault":false}`, expectedRes: []byte(""), }, @@ -178,17 +373,28 @@ func TestResolve(t *testing.T) { kind: "task", imageName: "foo", version: "baz", - catalog: "tekton", + catalog: "Tekton", + hubType: TektonHubType, input: `value`, - expectedErr: fmt.Errorf("error unmarshalling json response: invalid character 'v' looking for beginning of value"), + expectedErr: fmt.Errorf("fail to fetch Tekton Hub resource: error unmarshalling json response: invalid character 'v' looking for beginning of value"), }, { - name: "response with empty body error", + name: "response with empty body error from Tekton Hub", kind: "task", imageName: "foo", version: "baz", - catalog: "tekton", - expectedErr: fmt.Errorf("error unmarshalling json response: unexpected end of JSON input"), + catalog: "Tekton", + hubType: TektonHubType, + expectedErr: fmt.Errorf("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: fmt.Errorf("fail to fetch Artifact Hub resource: error unmarshalling json response: unexpected end of JSON input"), }, } @@ -198,16 +404,28 @@ func TestResolve(t *testing.T) { fmt.Fprintf(w, tc.input) })) - resolver := &Resolver{HubURL: svr.URL + "/" + YamlEndpoint} + resolver := &Resolver{ + TektonHubURL: svr.URL + "/" + TektonHubYamlEndpoint, + ArtifactHubURL: svr.URL + "/" + ArtifactHubYamlEndpoint, + } params := map[string]string{ ParamKind: tc.kind, ParamName: tc.imageName, ParamVersion: tc.version, ParamCatalog: tc.catalog, + ParamType: tc.hubType, + } + + 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", } + ctx := framework.InjectResolverConfigToContext(resolverContext(), config) - output, err := resolver.Resolve(resolverContext(), toParams(params)) + output, err := resolver.Resolve(ctx, toParams(params)) if tc.expectedErr != nil { if err == nil { t.Fatalf("expected err '%v' but didn't get one", tc.expectedErr) diff --git a/test/resolvers_test.go b/test/resolvers_test.go index f3a2f42f322..874a29e3851 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -190,7 +190,7 @@ spec: if err := WaitForPipelineRunState(ctx, c, prName, timeout, Chain( FailedWithReason(pod.ReasonCouldntGetTask, prName), - FailedWithMessage("requested resource 'https://api.hub.tekton.dev/v1/resource/Tekton/task/git-clone-this-does-not-exist/0.7/yaml' not found on hub", prName), + FailedWithMessage("requested resource 'https://artifacthub.io/api/v1/packages/tekton-task/tekton-catalog-tasks/git-clone-this-does-not-exist/0.7.0' not found on hub", prName), ), "PipelineRunFailed"); err != nil { t.Fatalf("Error waiting for PipelineRun to finish with expected error: %s", err) }