diff --git a/cmd/resolvers/main.go b/cmd/resolvers/main.go index 18de0c0f1a3..6de254f5b85 100644 --- a/cmd/resolvers/main.go +++ b/cmd/resolvers/main.go @@ -17,6 +17,10 @@ limitations under the License. package main import ( + "fmt" + "os" + "strings" + "github.com/tektoncd/pipeline/pkg/apis/resolution/v1alpha1" "github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" @@ -30,8 +34,20 @@ 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 + } + fmt.Println("RUNNING WITH HUB URL PATTERN:", hubURL) + sharedmain.MainWithContext(ctx, "controller", framework.NewController(ctx, &git.Resolver{}), - framework.NewController(ctx, &hub.Resolver{}), + framework.NewController(ctx, &hub.Resolver{HubURL: hubURL}), framework.NewController(ctx, &bundle.Resolver{})) } diff --git a/config/config-feature-flags.yaml b/config/config-feature-flags.yaml index 2c0619c1f4c..59e7e72623e 100644 --- a/config/config-feature-flags.yaml +++ b/config/config-feature-flags.yaml @@ -81,3 +81,15 @@ data: # Setting this flag to "true" enables CloudEvents for Runs, as long as a # CloudEvents sink is configured in the config-defaults config map send-cloudevents-for-runs: "false" + # Setting this flag to "true" enables remote resolution of Tekton OCI bundles. + # This is an experimental feature and thus should still be considered + # an alpha feature. + enable-bundles-resolver: "false" + # Setting this flag to "true" enables remote resolution of tasks and pipelines via the Tekton Hub. + # This is an experimental feature and thus should still be considered + # an alpha feature. + enable-hub-resolver: "false" + # Setting this flag to "true" enables remote resolution of tasks and pipelines from Git repositories. + # This is an experimental feature and thus should still be considered + # an alpha feature. + enable-git-resolver: "false" diff --git a/config/resolvers/200-clusterrole.yaml b/config/resolvers/200-clusterrole.yaml new file mode 100644 index 00000000000..525dcaa90f2 --- /dev/null +++ b/config/resolvers/200-clusterrole.yaml @@ -0,0 +1,27 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + # ClusterRole for resolvers to monitor and update resolutionrequests. + name: tekton-pipelines-resolvers-resolution-request-updates + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +rules: + - apiGroups: ["resolution.tekton.dev"] + resources: ["resolutionrequests", "resolutionrequests/status"] + verbs: ["get", "list", "watch", "update", "patch"] diff --git a/config/resolvers/200-role.yaml b/config/resolvers/200-role.yaml new file mode 100644 index 00000000000..8f1ae79c9aa --- /dev/null +++ b/config/resolvers/200-role.yaml @@ -0,0 +1,33 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: tekton-pipelines-resolvers-namespace-rbac + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +rules: + # Needed to watch and load configuration and secret data. + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list", "update", "watch"] + + # This is needed by leader election to run the controller in HA. + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "create", "update", "delete", "patch", "watch"] diff --git a/config/resolvers/200-serviceaccount.yaml b/config/resolvers/200-serviceaccount.yaml new file mode 100644 index 00000000000..da04a8a56fd --- /dev/null +++ b/config/resolvers/200-serviceaccount.yaml @@ -0,0 +1,23 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: tekton-pipelines-resolvers + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines diff --git a/config/resolvers/201-clusterrolebinding.yaml b/config/resolvers/201-clusterrolebinding.yaml new file mode 100644 index 00000000000..1a67521fb6a --- /dev/null +++ b/config/resolvers/201-clusterrolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2021 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tekton-pipelines-resolvers + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +subjects: + - kind: ServiceAccount + name: tekton-pipelines-resolvers + namespace: tekton-pipelines +roleRef: + kind: ClusterRole + name: tekton-pipelines-resolvers-resolution-request-updates + apiGroup: rbac.authorization.k8s.io diff --git a/config/resolvers/201-rolebinding.yaml b/config/resolvers/201-rolebinding.yaml new file mode 100644 index 00000000000..28ec2e866e0 --- /dev/null +++ b/config/resolvers/201-rolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2021 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: tekton-pipelines-resolvers-namespace-rbac + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +subjects: + - kind: ServiceAccount + name: tekton-pipelines-resolvers + namespace: tekton-pipelines +roleRef: + kind: Role + name: tekton-pipelines-resolvers-namespace-rbac + apiGroup: rbac.authorization.k8s.io diff --git a/config/resolvers/bundleresolver-config.yaml b/config/resolvers/bundleresolver-config.yaml new file mode 100644 index 00000000000..704c0d9faae --- /dev/null +++ b/config/resolvers/bundleresolver-config.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: bundleresolver-config + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +data: + # the default service account name to use for bundle requests. + default-service-account: "default" + # The default layer kind in the bundle image. + default-kind: "task" diff --git a/config/resolvers/git-resolver-config.yaml b/config/resolvers/git-resolver-config.yaml new file mode 100644 index 00000000000..8a57e262cd7 --- /dev/null +++ b/config/resolvers/git-resolver-config.yaml @@ -0,0 +1,30 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: git-resolver-config + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +data: + # The maximum amount of time a single git resolution may take. + fetch-timeout: "1m" + # The git url to fetch the remote resource from. + default-url: "https://github.com/tektoncd/catalog.git" + # The git revision to fetch the remote resource from. + default-revision: "main" diff --git a/config/resolvers/hubresolver-config.yaml b/config/resolvers/hubresolver-config.yaml new file mode 100644 index 00000000000..49ae75593a5 --- /dev/null +++ b/config/resolvers/hubresolver-config.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: hubresolver-config + namespace: tekton-pipelines + labels: + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines +data: + # the default catalog from where to pull the resource. + default-catalog: "Tekton" + # The default layer kind in the hub image. + default-kind: "task" diff --git a/config/resolvers/resolvers-deployment.yaml b/config/resolvers/resolvers-deployment.yaml new file mode 100644 index 00000000000..d5b95db5539 --- /dev/null +++ b/config/resolvers/resolvers-deployment.yaml @@ -0,0 +1,110 @@ +# 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. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tekton-pipelines-remote-resolvers + namespace: tekton-pipelines + labels: + app.kubernetes.io/name: resolvers + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/version: "devel" + app.kubernetes.io/part-of: tekton-pipelines + # tekton.dev/release value replaced with inputs.params.versionTag in pipeline/tekton/publish.yaml + pipeline.tekton.dev/release: "devel" + # labels below are related to istio and should not be used for resource lookup + version: "devel" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: resolvers + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines + template: + metadata: + labels: + app.kubernetes.io/name: resolvers + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/version: "devel" + app.kubernetes.io/part-of: tekton-pipelines + # tekton.dev/release value replaced with inputs.params.versionTag in pipeline/tekton/publish.yaml + pipeline.tekton.dev/release: "devel" + # labels below are related to istio and should not be used for resource lookup + app: tekton-pipelines-resolvers + version: "devel" + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: resolvers + app.kubernetes.io/component: resolvers + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-pipelines + topologyKey: kubernetes.io/hostname + weight: 100 + serviceAccountName: tekton-pipelines-resolvers + containers: + - name: controller + image: ko://github.com/tektoncd/pipeline/cmd/resolvers + resources: + requests: + cpu: 100m + memory: 100Mi + limits: + cpu: 1000m + memory: 1000Mi + ports: + - name: metrics + containerPort: 9090 + env: + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # If you are changing these names, you will also need to update + # the controller's Role in 200-role.yaml to include the new + # values in the "configmaps" "get" rule. + - name: CONFIG_DEFAULTS_NAME + value: config-defaults + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: CONFIG_OBSERVABILITY_NAME + value: config-observability + - name: CONFIG_ARTIFACT_BUCKET_NAME + value: config-artifact-bucket + - name: CONFIG_ARTIFACT_PVC_NAME + value: config-artifact-pvc + - name: CONFIG_FEATURE_FLAGS_NAME + value: feature-flags + - name: CONFIG_LEADERELECTION_NAME + value: config-leader-election + - 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/" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - all diff --git a/pkg/resolution/resolver/hub/resolver.go b/pkg/resolution/resolver/hub/resolver.go index 1d5f30eb4bd..ef72dfc24bf 100644 --- a/pkg/resolution/resolver/hub/resolver.go +++ b/pkg/resolution/resolver/hub/resolver.go @@ -26,11 +26,13 @@ import ( "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" ) -const disabledError = "cannot handle resolution request, enable-hub-resolver feature flag not true" +const ( + // LabelValueHubResolverType is the value to use for the + // resolution.tekton.dev/type label on resource requests + LabelValueHubResolverType string = "hub" -// LabelValueHubResolverType is the value to use for the -// resolution.tekton.dev/type label on resource requests -const LabelValueHubResolverType string = "hub" + 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 { @@ -121,7 +123,12 @@ func (r *Resolver) Resolve(ctx context.Context, params map[string]string) (frame if err != nil { return nil, fmt.Errorf("error requesting resource from hub: %w", err) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("requested resource '%s' not found on hub", url) + } + defer func() { + _ = resp.Body.Close() + }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) diff --git a/tekton/publish.yaml b/tekton/publish.yaml index 8556f1757d6..d3e4915dd5f 100644 --- a/tekton/publish.yaml +++ b/tekton/publish.yaml @@ -12,6 +12,9 @@ spec: - name: images description: List of cmd/* paths to be published as images default: "controller webhook entrypoint nop kubeconfigwriter git-init imagedigestexporter pullrequest-init workingdirinit" + - name: resolverImages + description: List of cmd/* paths to be published as images in release manifest resolvers.yaml + default: "resolvers" - name: versionTag description: The vX.Y.Z version that the artifacts should be tagged with (including `v`) - name: imageRegistry @@ -152,15 +155,18 @@ spec: # Rewrite "devel" to params.versionTag sed -i -e 's/\(pipeline.tekton.dev\/release\): "devel"/\1: "$(params.versionTag)"/g' -e 's/\(app.kubernetes.io\/version\): "devel"/\1: "$(params.versionTag)"/g' -e 's/\(version\): "devel"/\1: "$(params.versionTag)"/g' ${PROJECT_ROOT}/config/*.yaml + sed -i -e 's/\(pipeline.tekton.dev\/release\): "devel"/\1: "$(params.versionTag)"/g' -e 's/\(app.kubernetes.io\/version\): "devel"/\1: "$(params.versionTag)"/g' -e 's/\(version\): "devel"/\1: "$(params.versionTag)"/g' ${PROJECT_ROOT}/config/resolvers/*.yaml # Publish images and create release.yaml mkdir -p $OUTPUT_RELEASE_DIR - ko resolve --platform=$(params.platforms) --preserve-import-paths -t $(params.versionTag) -R -f ${PROJECT_ROOT}/config/ > $OUTPUT_RELEASE_DIR/release.yaml + ko resolve --platform=$(params.platforms) --preserve-import-paths -t $(params.versionTag) -l 'app.kubernetes.io/component!=resolvers' -R -f ${PROJECT_ROOT}/config/ > $OUTPUT_RELEASE_DIR/release.yaml + ko resolve --platform=$(params.platforms) --preserve-import-paths -t $(params.versionTag) -f ${PROJECT_ROOT}/config/resolvers > $OUTPUT_RELEASE_DIR/resolvers.yaml # Publish images and create release.notags.yaml # This is useful if your container runtime doesn't support the `image-reference:tag@digest` notation # This is currently the case for `cri-o` (and most likely others) - ko resolve --platform=$(params.platforms) --preserve-import-paths -R -f ${PROJECT_ROOT}/config/ > $OUTPUT_RELEASE_DIR/release.notags.yaml + ko resolve --platform=$(params.platforms) --preserve-import-paths -l 'app.kubernetes.io/component!=resolvers' -R -f ${PROJECT_ROOT}/config/ > $OUTPUT_RELEASE_DIR/release.notags.yaml + ko resolve --platform=$(params.platforms) --preserve-import-paths -f ${PROJECT_ROOT}/config/resolvers > $OUTPUT_RELEASE_DIR/resolvers.notags.yaml - name: koparse image: gcr.io/tekton-releases/dogfooding/koparse:latest @@ -179,6 +185,16 @@ spec: --path $OUTPUT_RELEASE_DIR/release.yaml \ --base ${IMAGES_PATH} --images ${IMAGES} > /workspace/built_images + for cmd in $(params.resolverImages) + do + RESOLVER_IMAGES="${RESOLVER_IMAGES} ${IMAGES_PATH}/cmd/${cmd}:$(params.versionTag)" + done + + # Parse the built images from the resolvers.yaml generated by ko + koparse \ + --path $OUTPUT_RELEASE_DIR/resolvers.yaml \ + --base ${IMAGES_PATH} --images ${RESOLVER_IMAGES} >> /workspace/built_images + - name: tag-images image: gcr.io/go-containerregistry/crane:debug script: | diff --git a/tekton/release-pipeline.yaml b/tekton/release-pipeline.yaml index 22b977a42f8..985143cab59 100644 --- a/tekton/release-pipeline.yaml +++ b/tekton/release-pipeline.yaml @@ -50,6 +50,12 @@ spec: - name: release-file-no-tag description: the URL of the release file value: $(tasks.report-bucket.results.release-no-tag) + - name: resolvers-file + description: the URL of the resolvers release file + value: $(tasks.report-bucket.results.resolvers) + - name: resolvers-file-no-tag + description: the URL of the resolvers release file + value: $(tasks.report-bucket.results.resolvers-no-tag) tasks: - name: git-clone taskRef: @@ -186,6 +192,10 @@ spec: description: The full URL of the release file in the bucket - name: release-no-tag description: The full URL of the release file (no tag) in the bucket + - name: resolvers + description: The full URL of the resolvers release file in the bucket + - name: resolvers-no-tag + description: The full URL of the resolvers file (no tag) in the bucket steps: - name: create-results image: alpine @@ -200,3 +210,5 @@ spec: BASE_URL=$(echo ${BASE_URL} | sed 's,gs://,https://storage.googleapis.com/,g') echo "${BASE_URL}/release.yaml" > $(results.release.path) echo "${BASE_URL}/release.notag.yaml" > $(results.release-no-tag.path) + echo "${BASE_URL}/resolvers.yaml" > $(results.resolvers.path) + echo "${BASE_URL}/resolvers.notags.yaml" > $(results.resolvers-no-tag.path) diff --git a/test/e2e-common.sh b/test/e2e-common.sh index 585a96c0f54..1cd0f8a4254 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -21,11 +21,18 @@ source $(git rev-parse --show-toplevel)/vendor/github.com/tektoncd/plumbing/scri function install_pipeline_crd() { echo ">> Deploying Tekton Pipelines" local ko_target="$(mktemp)" - ko resolve -R -f config/ > "${ko_target}" || fail_test "Pipeline image resolve failed" + ko resolve -l 'app.kubernetes.io/component!=resolvers' -R -f config/ > "${ko_target}" || fail_test "Pipeline image resolve failed" cat "${ko_target}" | sed -e 's%"level": "info"%"level": "debug"%' \ | sed -e 's%loglevel.controller: "info"%loglevel.controller: "debug"%' \ | sed -e 's%loglevel.webhook: "info"%loglevel.webhook: "debug"%' \ | kubectl apply -R -f - || fail_test "Build pipeline installation failed" + + verify_pipeline_installation + + if [ "${PIPELINE_FEATURE_GATE}" == "alpha" ]; then + ko apply -f config/resolvers || fail_test "Resolvers installation failed" + fi + verify_pipeline_installation export SYSTEM_NAMESPACE=tekton-pipelines @@ -35,6 +42,11 @@ function install_pipeline_crd() { function install_pipeline_crd_version() { echo ">> Deploying Tekton Pipelines of Version $1" kubectl apply -f "https://github.com/tektoncd/pipeline/releases/download/$1/release.yaml" || fail_test "Build pipeline installation failed of Version $1" + + if [ "${PIPELINE_FEATURE_GATE}" == "alpha" ]; then + kubectl apply -f "https://github.com/tektoncd/pipeline/releases/download/$1/resolvers.yaml" || fail_test "Resolvers installation failed of Version $1" + fi + verify_pipeline_installation } @@ -58,6 +70,10 @@ function uninstall_pipeline_crd_version() { echo ">> Uninstalling Tekton Pipelines of version $1" kubectl delete --ignore-not-found=true -f "https://github.com/tektoncd/pipeline/releases/download/$1/release.yaml" + if [ "${PIPELINE_FEATURE_GATE}" == "alpha" ]; then + kubectl delete --ignore-not-found=true -f "https://github.com/tektoncd/pipeline/releases/download/$1/resolvers.yaml" + fi + # Make sure that everything is cleaned up in the current namespace. delete_pipeline_resources } @@ -66,4 +82,8 @@ function delete_pipeline_resources() { for res in pipelineresources tasks clustertasks pipelines taskruns pipelineruns; do kubectl delete --ignore-not-found=true ${res}.tekton.dev --all done + + if [ "${PIPELINE_FEATURE_GATE}" == "alpha" ]; then + kubectl delete --ignore-not-found=true resolutionrequests.resolution.tekton.dev --all + fi } diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index 7a23a8016ee..9ed533d25f8 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -43,12 +43,16 @@ failed=0 function set_feature_gate() { local gate="$1" + local resolver="false" if [ "$gate" != "alpha" ] && [ "$gate" != "stable" ] && [ "$gate" != "beta" ] ; then printf "Invalid gate %s\n" ${gate} exit 255 fi + if [ "$gate" == "alpha" ]; then + resolver="true" + fi printf "Setting feature gate to %s\n", ${gate} - jsonpatch=$(printf "{\"data\": {\"enable-api-fields\": \"%s\"}}" $1) + jsonpatch=$(printf "{\"data\": {\"enable-api-fields\": \"%s\", \"enable-git-resolver\": \"%s\", \"enable-hub-resolver\": \"%s\"}}" $1 "${resolver}" "${resolver}") echo "feature-flags ConfigMap patch: ${jsonpatch}" kubectl patch configmap feature-flags -n tekton-pipelines -p "$jsonpatch" } diff --git a/test/featureflags.go b/test/featureflags.go index e901e2fba84..48632378394 100644 --- a/test/featureflags.go +++ b/test/featureflags.go @@ -35,6 +35,29 @@ func requireAnyGate(gates map[string]string) func(context.Context, *testing.T, * } } +// requireAllgates returns a setup func that will skip the current +// test if all of the feature-flags in the given map don't match +// what's in the feature-flags ConfigMap. It will fatally fail +// the test if it cannot get the feature-flag configmap. +func requireAllGates(gates map[string]string) func(context.Context, *testing.T, *clients, string) { + return func(ctx context.Context, t *testing.T, c *clients, namespace string) { + featureFlagsCM, err := c.KubeClient.CoreV1().ConfigMaps(system.Namespace()).Get(ctx, config.GetFeatureFlagsConfigName(), metav1.GetOptions{}) + if err != nil { + t.Fatalf("Failed to get ConfigMap `%s`: %s", config.GetFeatureFlagsConfigName(), err) + } + pairs := []string{} + for name, value := range gates { + actual, ok := featureFlagsCM.Data[name] + if !ok || value != actual { + pairs = append(pairs, fmt.Sprintf("%q is %q, want %s", name, actual, value)) + } + } + if len(pairs) > 0 { + t.Skipf("One or more feature flags not matching required: %s", strings.Join(pairs, "; ")) + } + } +} + // GetEmbeddedStatus gets the current value for the "embedded-status" feature flag. // If the flag is not set, it returns the default value. func GetEmbeddedStatus(ctx context.Context, t *testing.T, kubeClient kubernetes.Interface) string { diff --git a/test/presubmit-tests.sh b/test/presubmit-tests.sh index fd9ac1e3838..76cb5b0a888 100755 --- a/test/presubmit-tests.sh +++ b/test/presubmit-tests.sh @@ -72,7 +72,8 @@ function ko_resolve() { github.com/tektoncd/pipeline/cmd/git-init: distroless.dev/git EOF - KO_DOCKER_REPO=example.com ko resolve --platform=all --push=false -R -f config 1>/dev/null + KO_DOCKER_REPO=example.com ko resolve -l 'app.kubernetes.io/component!=resolvers' --platform=all --push=false -R -f config 1>/dev/null + KO_DOCKER_REPO=example.com ko resolve --platform=all --push=false -f config/resolvers 1>/dev/null } function post_build_tests() { diff --git a/test/resolvers_test.go b/test/resolvers_test.go new file mode 100644 index 00000000000..66085223a3c --- /dev/null +++ b/test/resolvers_test.go @@ -0,0 +1,273 @@ +//go:build e2e +// +build e2e + +/* + 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 test + +import ( + "context" + "fmt" + "testing" + + "github.com/tektoncd/pipeline/pkg/pod" + "github.com/tektoncd/pipeline/test/parse" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + knativetest "knative.dev/pkg/test" + "knative.dev/pkg/test/helpers" +) + +var hubFeatureFlags = requireAllGates(map[string]string{ + "enable-hub-resolver": "true", + "enable-api-fields": "alpha", +}) + +var gitFeatureFlags = requireAllGates(map[string]string{ + "enable-git-resolver": "true", + "enable-api-fields": "alpha", +}) + +func TestHubResolver(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t, hubFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + prName := helpers.ObjectNameForTest(t) + + pipelineRun := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + workspaces: + - name: output # this workspace name must be declared in the Pipeline + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce # access mode may affect how you can use this volume in parallel tasks + resources: + requests: + storage: 1Gi + pipelineSpec: + workspaces: + - name: output + tasks: + - name: task1 + workspaces: + - name: output + taskRef: + resolver: hub + params: + - name: kind + value: task + - name: name + value: git-clone + - name: version + value: "0.7" + params: + - name: url + value: https://github.com/tektoncd/pipeline + - name: deleteExisting + value: "true" +`, prName, namespace)) + + _, err := c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, PipelineRunSucceed(prName), "PipelineRunSuccess"); err != nil { + t.Fatalf("Error waiting for PipelineRun %s to finish: %s", prName, err) + } + +} + +func TestHubResolver_Failure(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t, hubFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + prName := helpers.ObjectNameForTest(t) + + pipelineRun := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + workspaces: + - name: output # this workspace name must be declared in the Pipeline + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce # access mode may affect how you can use this volume in parallel tasks + resources: + requests: + storage: 1Gi + pipelineSpec: + workspaces: + - name: output + tasks: + - name: task1 + workspaces: + - name: output + taskRef: + resolver: hub + params: + - name: kind + value: task + - name: name + value: git-clone-this-does-not-exist + - name: version + value: "0.7" + params: + - name: url + value: https://github.com/tektoncd/pipeline + - name: deleteExisting + value: "true" +`, prName, namespace)) + + _, err := c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, + Chain( + FailedWithReason(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), + ), "PipelineRunFailed"); err != nil { + t.Fatalf("Error waiting for PipelineRun to finish with expected error: %s", err) + } +} + +func TestGitResolver_Failure(t *testing.T) { + defaultURL := "https://github.com/tektoncd/catalog.git" + defaultPathInRepo := "/task/git-clone/0.7/git-clone.yaml" + defaultCommit := "783b4fe7d21148f3b1a93bfa49b0024d8c6c2955" + + testCases := []struct { + name string + url string + pathInRepo string + commit string + expectedErr string + }{ + { + name: "repo does not exist", + url: "https://github.com/tektoncd/catalog-does-not-exist.git", + expectedErr: "clone error: authentication required", + }, { + name: "path does not exist", + pathInRepo: "/task/banana/55.55/banana.yaml", + expectedErr: "error opening file \"/task/banana/55.55/banana.yaml\": file does not exist", + }, { + name: "commit does not exist", + commit: "abcd0123", + expectedErr: "revision error: reference not found", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expectedErr := tc.expectedErr + url := tc.url + if url == "" { + url = defaultURL + } + pathInRepo := tc.pathInRepo + if pathInRepo == "" { + pathInRepo = defaultPathInRepo + } + commit := tc.commit + if commit == "" { + commit = defaultCommit + } + + ctx := context.Background() + c, namespace := setup(ctx, t, gitFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + prName := helpers.ObjectNameForTest(t) + + pipelineRun := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + workspaces: + - name: output # this workspace name must be declared in the Pipeline + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce # access mode may affect how you can use this volume in parallel tasks + resources: + requests: + storage: 1Gi + pipelineSpec: + workspaces: + - name: output + tasks: + - name: task1 + workspaces: + - name: output + taskRef: + resolver: git + params: + - name: url + value: %s + - name: pathInRepo + value: %s + - name: revision + value: %s + params: + - name: url + value: https://github.com/tektoncd/pipeline + - name: deleteExisting + value: "true" +`, prName, namespace, url, pathInRepo, commit)) + + _, err := c.PipelineRunClient.Create(ctx, pipelineRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create PipelineRun `%s`: %s", prName, err) + } + + t.Logf("Waiting for PipelineRun %s in namespace %s to complete", prName, namespace) + if err := WaitForPipelineRunState(ctx, c, prName, timeout, + Chain( + FailedWithReason(pod.ReasonCouldntGetTask, prName), + FailedWithMessage(expectedErr, prName), + ), "PipelineRunFailed"); err != nil { + t.Fatalf("Error waiting for PipelineRun to finish with expected error: %s", err) + } + }) + } +} diff --git a/test/tektonbundles_test.go b/test/tektonbundles_test.go index 88282131392..be8f46e81d2 100644 --- a/test/tektonbundles_test.go +++ b/test/tektonbundles_test.go @@ -193,6 +193,154 @@ spec: } } +// TestTektonBundlesResolver is an integration test which tests a simple, working Tekton bundle using OCI +// images using the remote resolution bundles resolver. +func TestTektonBundlesResolver(t *testing.T) { + ctx := context.Background() + c, namespace := setup(ctx, t, withRegistry, requireFeatureFlags) + + t.Parallel() + + knativetest.CleanupOnInterrupt(func() { tearDown(ctx, t, c, namespace) }, t.Logf) + defer tearDown(ctx, t, c, namespace) + + taskName := helpers.ObjectNameForTest(t) + pipelineName := helpers.ObjectNameForTest(t) + pipelineRunName := helpers.ObjectNameForTest(t) + repo := fmt.Sprintf("%s:5000/tektonbundlesresolver", getRegistryServiceIP(ctx, t, c, namespace)) + ref, err := name.ParseReference(repo) + if err != nil { + t.Fatalf("Failed to parse %s as an OCI reference: %s", repo, err) + } + + task := parse.MustParseTask(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + steps: + - name: hello + image: alpine + script: 'echo Hello' +`, taskName, namespace)) + + pipeline := parse.MustParsePipeline(t, fmt.Sprintf(` +metadata: + name: %s + namespace: %s +spec: + tasks: + - name: hello-world + taskRef: + resolver: bundles + params: + - name: bundle + value: %s + - name: name + value: %s +`, pipelineName, namespace, repo, taskName)) + + // Write the task and pipeline into an image to the registry in the proper format. + rawTask, err := yaml.Marshal(task) + if err != nil { + t.Fatalf("Failed to marshal task to yaml: %s", err) + } + + rawPipeline, err := yaml.Marshal(pipeline) + if err != nil { + t.Fatalf("Failed to marshal task to yaml: %s", err) + } + + img := empty.Image + taskLayer, err := tarball.LayerFromReader(bytes.NewBuffer(rawTask)) + if err != nil { + t.Fatalf("Failed to create oci layer from task: %s", err) + } + pipelineLayer, err := tarball.LayerFromReader(bytes.NewBuffer(rawPipeline)) + if err != nil { + t.Fatalf("Failed to create oci layer from pipeline: %s", err) + } + img, err = mutate.Append(img, mutate.Addendum{ + Layer: taskLayer, + Annotations: map[string]string{ + "dev.tekton.image.name": taskName, + "dev.tekton.image.kind": strings.ToLower(task.Kind), + "dev.tekton.image.apiVersion": task.APIVersion, + }, + }, mutate.Addendum{ + Layer: pipelineLayer, + Annotations: map[string]string{ + "dev.tekton.image.name": pipelineName, + "dev.tekton.image.kind": strings.ToLower(pipeline.Kind), + "dev.tekton.image.apiVersion": pipeline.APIVersion, + }, + }) + if err != nil { + t.Fatalf("Failed to create an oci image from the task and pipeline layers: %s", err) + } + + // Publish this image to the in-cluster registry. + publishImg(ctx, t, c, namespace, img, ref) + + // Now generate a PipelineRun to invoke this pipeline and task. + pr := parse.MustParsePipelineRun(t, fmt.Sprintf(` +metadata: + name: %s +spec: + pipelineRef: + resolver: bundles + params: + - name: bundle + value: %s + - name: name + value: %s + - name: kind + value: pipeline +`, pipelineRunName, repo, pipelineName)) + if _, err := c.PipelineRunClient.Create(ctx, pr, metav1.CreateOptions{}); err != nil { + t.Fatalf("Failed to create PipelineRun: %s", err) + } + + t.Logf("Waiting for PipelineRun in namespace %s to finish", namespace) + if err := WaitForPipelineRunState(ctx, c, pipelineRunName, timeout, PipelineRunSucceed(pipelineRunName), "PipelineRunCompleted"); err != nil { + t.Errorf("Error waiting for PipelineRun to finish with error: %s", err) + } + + trs, err := c.TaskRunClient.List(ctx, metav1.ListOptions{}) + if err != nil { + t.Errorf("Error retrieving taskrun: %s", err) + } + if len(trs.Items) != 1 { + t.Fatalf("Expected 1 TaskRun but found %d", len(trs.Items)) + } + + tr := trs.Items[0] + if tr.Status.GetCondition(apis.ConditionSucceeded).IsFalse() { + t.Errorf("Expected TaskRun to succeed but instead found condition: %s", tr.Status.GetCondition(apis.ConditionSucceeded)) + } + + if tr.Status.PodName == "" { + t.Fatal("Error getting a PodName (empty)") + } + p, err := c.KubeClient.CoreV1().Pods(namespace).Get(ctx, tr.Status.PodName, metav1.GetOptions{}) + + if err != nil { + t.Fatalf("Error getting pod `%s` in namespace `%s`", tr.Status.PodName, namespace) + } + for _, stat := range p.Status.ContainerStatuses { + if strings.Contains(stat.Name, "step-hello") { + req := c.KubeClient.CoreV1().Pods(namespace).GetLogs(p.Name, &corev1.PodLogOptions{Container: stat.Name}) + logContent, err := req.Do(ctx).Raw() + if err != nil { + t.Fatalf("Error getting pod logs for pod `%s` and container `%s` in namespace `%s`", tr.Status.PodName, stat.Name, namespace) + } + if !strings.Contains(string(logContent), "Hello") { + t.Fatalf("Expected logs to say hello but received %v", logContent) + } + } + } +} + // TestTektonBundlesUsingRegularImage is an integration test which passes a non-Tekton bundle as a task reference. func TestTektonBundlesUsingRegularImage(t *testing.T) { ctx := context.Background()