diff --git a/go.mod b/go.mod index eca9126551..4422720bdf 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,6 @@ require ( golang.org/x/sync v0.4.0 golang.org/x/term v0.13.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.4.0 k8s.io/api v0.27.6 k8s.io/apimachinery v0.27.6 @@ -242,6 +241,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.27.6 // indirect k8s.io/cli-runtime v0.25.9 // indirect k8s.io/klog/v2 v2.100.1 // indirect diff --git a/pkg/pipelines/resources/tekton/task/func-s2i/0.2/func-s2i.yaml b/pkg/pipelines/resources/tekton/task/func-s2i/0.2/func-s2i.yaml new file mode 100644 index 0000000000..9a2f7a3b92 --- /dev/null +++ b/pkg/pipelines/resources/tekton/task/func-s2i/0.2/func-s2i.yaml @@ -0,0 +1,136 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: func-s2i + labels: + app.kubernetes.io/version: "0.1" + annotations: + tekton.dev/pipelines.minVersion: "0.17.0" + tekton.dev/categories: Image Build + tekton.dev/tags: image-build + tekton.dev/platforms: "linux/amd64" +spec: + description: >- + Knative Functions Source-to-Image (S2I) is a toolkit and workflow for building reproducible + container images from source code + + S2I produces images by injecting source code into a base S2I container image + and letting the container prepare that source code for execution. The base + S2I container images contains the language runtime and build tools needed for + building and running the source code. + + params: + - name: BUILDER_IMAGE + description: The location of the s2i builder image. + - name: APP_IMAGE + description: Reference of the image S2I will produce. + - name: REGISTRY + description: The registry associated with the function image. + - name: SOURCE_SUBPATH + description: The location of the path to run s2i from. + default: . + - name: TLSVERIFY + description: Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry) + default: "true" + - name: LOGLEVEL + description: Log level when running the S2I binary + default: "0" + - name: ENV_VARS + type: array + description: Environment variables to set during _build-time_. + default: [] + - name: S2I_IMAGE_SCRIPTS_URL + description: The URL containing the default assemble and run scripts for the builder image. + default: "image:///usr/libexec/s2i" + workspaces: + - name: source + - name: cache + description: Directory where cache is stored (e.g. local mvn repo). + optional: true + - name: sslcertdir + optional: true + - name: dockerconfig + description: >- + An optional workspace that allows providing a .docker/config.json file + for Buildah to access the container registry. + The file should be placed at the root of the Workspace with name config.json. + optional: true + results: + - name: IMAGE_DIGEST + description: Digest of the image just built. + steps: + - name: generate + image: quay.io/boson/s2i:latest + workingDir: $(workspaces.source.path) + args: ["$(params.ENV_VARS[*])"] + script: | + echo "Processing Build Environment Variables" + echo "" > /env-vars/env-file + for var in "$@" + do + if [[ "$var" != "=" ]]; then + echo "$var" >> /env-vars/env-file + fi + done + + echo "Generated Build Env Var file" + echo "------------------------------" + cat /env-vars/env-file + echo "------------------------------" + + /usr/local/bin/s2i --loglevel=$(params.LOGLEVEL) build $(params.SOURCE_SUBPATH) $(params.BUILDER_IMAGE) \ + --image-scripts-url $(params.S2I_IMAGE_SCRIPTS_URL) \ + --as-dockerfile /gen-source/Dockerfile.gen --environment-file /env-vars/env-file + + echo "Preparing func.yaml for later deployment" + func_file="$(workspaces.source.path)/func.yaml" + if [ "$(params.SOURCE_SUBPATH)" != "" ]; then + func_file="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)/func.yaml" + fi + sed -i "s|^registry:.*$|registry: $(params.REGISTRY)|" "$func_file" + echo "Function image registry: $(params.REGISTRY)" + + s2iignore_file="$(dirname "$func_file")/.s2iignore" + [ -f "$s2iignore_file" ] || echo "node_modules" >> "$s2iignore_file" + + volumeMounts: + - mountPath: /gen-source + name: gen-source + - mountPath: /env-vars + name: env-vars + - name: build + image: quay.io/buildah/stable:v1.31.0 + workingDir: /gen-source + script: | + TLS_VERIFY_FLAG="" + if [ "$(params.TLSVERIFY)" = "false" ] || [ "$(params.TLSVERIFY)" = "0" ]; then + TLS_VERIFY_FLAG="--tls-verify=false" + fi + + [[ "$(workspaces.sslcertdir.bound)" == "true" ]] && CERT_DIR_FLAG="--cert-dir $(workspaces.sslcertdir.path)" + ARTIFACTS_CACHE_PATH="$(workspaces.cache.path)/mvn-artifacts" + [ -d "${ARTIFACTS_CACHE_PATH}" ] || mkdir "${ARTIFACTS_CACHE_PATH}" + buildah ${CERT_DIR_FLAG} bud --storage-driver=vfs ${TLS_VERIFY_FLAG} --layers \ + -v "${ARTIFACTS_CACHE_PATH}:/tmp/artifacts/:rw,z,U" \ + -f /gen-source/Dockerfile.gen -t $(params.APP_IMAGE) . + + [[ "$(workspaces.dockerconfig.bound)" == "true" ]] && export DOCKER_CONFIG="$(workspaces.dockerconfig.path)" + buildah ${CERT_DIR_FLAG} push --storage-driver=vfs ${TLS_VERIFY_FLAG} --digestfile $(workspaces.source.path)/image-digest \ + $(params.APP_IMAGE) docker://$(params.APP_IMAGE) + + cat $(workspaces.source.path)/image-digest | tee /tekton/results/IMAGE_DIGEST + volumeMounts: + - name: varlibcontainers + mountPath: /var/lib/containers + - mountPath: /gen-source + name: gen-source + securityContext: + capabilities: + add: ["SETFCAP"] + volumes: + - emptyDir: {} + name: varlibcontainers + - emptyDir: {} + name: gen-source + - emptyDir: {} + name: env-vars diff --git a/pkg/pipelines/tekton/pipelines_integration_test.go b/pkg/pipelines/tekton/pipelines_integration_test.go index b10f66de53..d24c8847af 100644 --- a/pkg/pipelines/tekton/pipelines_integration_test.go +++ b/pkg/pipelines/tekton/pipelines_integration_test.go @@ -20,7 +20,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/func/pkg/k8s" - "knative.dev/func/pkg/builders/buildpacks" "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/pipelines/tekton" @@ -58,6 +57,7 @@ func TestOnClusterBuild(t *testing.T) { ns := setupNS(t) pp := tekton.NewPipelinesProvider( + tekton.WithPipelineDecorator(testDecorator{}), tekton.WithCredentialsProvider(credentialsProvider), tekton.WithNamespace(ns)) @@ -85,10 +85,46 @@ func TestOnClusterBuild(t *testing.T) { return } t.Log("call to knative service successful") + + // Check if labels are correct. + cli, err := tekton.NewTektonClients() + if err != nil { + t.Fatal(err) + } + pl, err := cli.Tekton.TektonV1beta1().Pipelines(ns).List(ctx, metav1.ListOptions{}) + if len(pl.Items) == 1 { + if val, ok := pl.Items[0].Labels["test-label-key"]; !ok || val != "test-label-value" { + t.Error("test label has not been set for pipeline") + } + } else { + t.Errorf("unexpected pipeline count: %d", len(pl.Items)) + } + prl, err := cli.Tekton.TektonV1beta1().PipelineRuns(ns).List(ctx, metav1.ListOptions{}) + if err != nil { + t.Fatal(err) + } + if len(prl.Items) == 1 { + if val, ok := prl.Items[0].Labels["test-label-key"]; !ok || val != "test-label-value" { + t.Error("test label has not been set for pipeline run") + } + } else { + t.Errorf("unexpected pipeline run count: %d", len(prl.Items)) + } }) } } +type testDecorator struct{} + +func (t testDecorator) UpdateLabels(function fn.Function, m map[string]string) map[string]string { + result := make(map[string]string, len(m)+1) + for k, v := range m { + result[k] = v + } + result["test-label-key"] = "test-label-value" + return result +} + func setupNS(t *testing.T) string { name := "pipeline-integration-test-" + strings.ToLower(random.AlphaString(5)) cliSet, err := k8s.NewKubernetesClientset() @@ -174,7 +210,7 @@ func createSimpleGoProject(t *testing.T, ns string) fn.Function { Invoke: "none", Build: fn.BuildSpec{ BuilderImages: map[string]string{ - "pack": buildpacks.DefaultTinyBuilder, + "pack": "index.docker.io/paketobuildpacks/builder-jammy-tiny", "s2i": "registry.access.redhat.com/ubi8/go-toolset", }, }, @@ -190,16 +226,40 @@ func createSimpleGoProject(t *testing.T, ns string) fn.Function { return f } -const simpleGOSvc = `package function +const simpleGOSvc = `package main import ( "context" + "net" "net/http" + "os" + "os/signal" + "syscall" ) -func Handle(ctx context.Context, resp http.ResponseWriter, req *http.Request) { - resp.Header().Add("Content-Type", "text/plain") - resp.WriteHeader(200) - _, _ = resp.Write([]byte("Hello World!\n")) +func main() { + sigs := make(chan os.Signal, 5) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + s := http.Server{ + Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Add("Content-Type", "text/plain") + resp.WriteHeader(200) + _, _ = resp.Write([]byte("OK")) + }), + } + go func() { + <-sigs + _ = s.Shutdown(context.Background()) + }() + port := "8080" + if p, ok := os.LookupEnv("PORT"); ok { + port = p + } + l, err := net.Listen("tcp4", ":"+port) + if err != nil { + panic(err) + } + _ = s.Serve(l) } ` diff --git a/pkg/pipelines/tekton/resources.go b/pkg/pipelines/tekton/resources.go index 583962c73a..2839b74be1 100644 --- a/pkg/pipelines/tekton/resources.go +++ b/pkg/pipelines/tekton/resources.go @@ -3,8 +3,14 @@ package tekton import ( "context" "fmt" + "os" + "path" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + coreV1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + k8sYaml "k8s.io/apimachinery/pkg/util/yaml" "knative.dev/func/pkg/builders" "knative.dev/func/pkg/builders/buildpacks" @@ -12,6 +18,440 @@ import ( fn "knative.dev/func/pkg/functions" ) +func getPipeline(f fn.Function, labels map[string]string) (*v1beta1.Pipeline, error) { + pipelineFromFile, err := loadResource[*v1beta1.Pipeline](path.Join(f.Root, resourcesDirectory, pipelineFileName)) + if err != nil { + return nil, fmt.Errorf("cannot load resource from file: %v", err) + } + if pipelineFromFile != nil { + name := getPipelineName(f) + if pipelineFromFile.Name != name { + return nil, fmt.Errorf("resource name missmatch: %q != %q", pipelineFromFile.Name, name) + } + return pipelineFromFile, nil + } + + var buildTaskSpec v1beta1.TaskSpec + switch f.Build.Builder { + case builders.S2I: + buildTaskSpec = *S2ITask.Spec.DeepCopy() + case builders.Pack: + buildTaskSpec = *BuildpackTask.Spec.DeepCopy() + default: + return nil, fmt.Errorf("unsupported builder: %q", f.Build.BuilderImages) + } + + tasks := []v1beta1.PipelineTask{ + v1beta1.PipelineTask{ + Name: "fetch-sources", + TaskRef: &v1beta1.TaskRef{ + ResolverRef: v1beta1.ResolverRef{ + Resolver: "hub", + Params: []v1beta1.Param{ + v1beta1.Param{ + Name: "kind", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "task", + }, + }, + v1beta1.Param{ + Name: "name", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "git-clone", + }, + }, + v1beta1.Param{ + Name: "version", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "0.4", + }, + }, + }, + }, + }, + Params: []v1beta1.Param{ + v1beta1.Param{ + Name: "url", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.gitRepository)", + }, + }, + v1beta1.Param{ + Name: "revision", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.gitRevision)", + }, + }, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + v1beta1.WorkspacePipelineTaskBinding{ + Name: "output", + Workspace: "source-workspace", + }, + }, + }, + v1beta1.PipelineTask{ + Name: "build", + TaskSpec: &v1beta1.EmbeddedTask{ + TaskSpec: buildTaskSpec, + }, + RunAfter: []string{"fetch-sources"}, + Params: []v1beta1.Param{ + v1beta1.Param{ + Name: "APP_IMAGE", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.imageName)", + }, + }, + v1beta1.Param{ + Name: "REGISTRY", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.registry)", + }, + }, + v1beta1.Param{ + Name: "SOURCE_SUBPATH", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.contextDir)", + }, + }, + v1beta1.Param{ + Name: "BUILDER_IMAGE", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.builderImage)", + }, + }, + v1beta1.Param{ + Name: "ENV_VARS", + Value: v1beta1.ParamValue{ + Type: "array", + ArrayVal: []string{ + "$(params.buildEnvs[*])", + }, + }, + }, + v1beta1.Param{ + Name: "S2I_IMAGE_SCRIPTS_URL", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.s2iImageScriptsUrl)", + }, + }, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + v1beta1.WorkspacePipelineTaskBinding{ + Name: "source", + Workspace: "source-workspace", + }, + v1beta1.WorkspacePipelineTaskBinding{ + Name: "cache", + Workspace: "cache-workspace", + }, + v1beta1.WorkspacePipelineTaskBinding{ + Name: "dockerconfig", + Workspace: "dockerconfig-workspace", + }, + }, + }, + v1beta1.PipelineTask{ + Name: "deploy", + TaskSpec: &v1beta1.EmbeddedTask{ + TaskSpec: *DeployTask.Spec.DeepCopy(), + }, + RunAfter: []string{"build"}, + Params: []v1beta1.Param{ + v1beta1.Param{ + Name: "path", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(workspaces.source.path)/$(params.contextDir)", + }, + }, + v1beta1.Param{ + Name: "image", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: "$(params.imageName)@$(tasks.build.results.IMAGE_DIGEST)", + }, + }, + }, + Workspaces: []v1beta1.WorkspacePipelineTaskBinding{ + v1beta1.WorkspacePipelineTaskBinding{ + Name: "source", + Workspace: "source-workspace", + }, + }, + }, + } + + if f.Build.Git.URL == "" { + tasks = tasks[1:] + tasks[0].RunAfter = nil + } + + result := v1beta1.Pipeline{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: getPipelineName(f), + Labels: labels, + Annotations: f.Deploy.Annotations, + }, + Spec: v1beta1.PipelineSpec{ + Tasks: tasks, + Params: []v1beta1.ParamSpec{ + v1beta1.ParamSpec{ + Name: "gitRepository", + Type: "string", + Description: "Git repository that hosts the function project", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "gitRevision", + Type: "string", + Description: "Git revision to build", + }, + v1beta1.ParamSpec{ + Name: "contextDir", + Type: "string", + Description: "Path where the function project is", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "imageName", + Type: "string", + Description: "Function image name", + }, + v1beta1.ParamSpec{ + Name: "registry", + Type: "string", + Description: "The registry associated with the function image", + }, + v1beta1.ParamSpec{ + Name: "builderImage", + Type: "string", + Description: "Builder image to be used", + }, + v1beta1.ParamSpec{ + Name: "buildEnvs", + Type: "array", + Description: "Environment variables to set during build time", + }, + v1beta1.ParamSpec{ + Name: "s2iImageScriptsUrl", + Type: "string", + Description: "URL containing the default assemble and run scripts for the builder image", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "image:///usr/libexec/s2i", + }, + }, + }, + Workspaces: []v1beta1.PipelineWorkspaceDeclaration{ + v1beta1.PipelineWorkspaceDeclaration{ + Name: "source-workspace", + Description: "Directory where function source is located.", + }, + v1beta1.PipelineWorkspaceDeclaration{ + Name: "cache-workspace", + Description: "Directory where build cache is stored.", + }, + v1beta1.PipelineWorkspaceDeclaration{ + Name: "dockerconfig-workspace", + Description: "Directory containing image registry credentials stored in config.json file.", + Optional: true, + }, + }, + }, + } + + return &result, nil +} + +func getPipelineRun(f fn.Function, labels map[string]string) (*v1beta1.PipelineRun, error) { + pipelineRunFromFile, err := loadResource[*v1beta1.PipelineRun](path.Join(f.Root, resourcesDirectory, pipelineRunFilenane)) + if err != nil { + return nil, fmt.Errorf("cannot load resource from file: %v", err) + } + if pipelineRunFromFile != nil { + generateName := getPipelineRunGenerateName(f) + if pipelineRunFromFile.GetGenerateName() != generateName { + return nil, fmt.Errorf("resource name missmatch: %q != %q", pipelineRunFromFile.GetGenerateName(), generateName) + } + return pipelineRunFromFile, nil + } + + if labels == nil { + labels = make(map[string]string, 1) + } + labels["tekton.dev/pipeline"] = getPipelineName(f) + + pipelinesTargetBranch := f.Build.Git.Revision + if pipelinesTargetBranch == "" { + pipelinesTargetBranch = defaultPipelinesTargetBranch + } + + contextDir := f.Build.Git.ContextDir + if contextDir == "" && f.Build.Builder == builders.S2I { + // TODO(lkingland): could instead update S2I to interpret empty string + // as cwd, such that builder-specific code can be kept out of here. + contextDir = "." + } + + buildEnvs := []string{} + if len(f.Build.BuildEnvs) == 0 { + buildEnvs = []string{"="} + } else { + for i := range f.Build.BuildEnvs { + buildEnvs = append(buildEnvs, f.Build.BuildEnvs[i].KeyValuePair()) + } + } + + s2iImageScriptsUrl := defaultS2iImageScriptsUrl + if f.Runtime == "quarkus" { + s2iImageScriptsUrl = quarkusS2iImageScriptsUrl + } + + result := v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "PipelineRun", + APIVersion: "tekton.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + GenerateName: getPipelineRunGenerateName(f), + Labels: labels, + Annotations: f.Deploy.Annotations, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{ + Name: getPipelineName(f), + }, + Params: []v1beta1.Param{ + v1beta1.Param{ + Name: "gitRepository", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: f.Build.Git.URL, + }, + }, + v1beta1.Param{ + Name: "gitRevision", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: pipelinesTargetBranch, + }, + }, + v1beta1.Param{ + Name: "contextDir", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: contextDir, + }, + }, + v1beta1.Param{ + Name: "imageName", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: f.Image, + }, + }, + v1beta1.Param{ + Name: "registry", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: f.Registry, + }, + }, + v1beta1.Param{ + Name: "builderImage", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: getBuilderImage(f), + }, + }, + v1beta1.Param{ + Name: "buildEnvs", + Value: v1beta1.ParamValue{ + Type: "array", + ArrayVal: buildEnvs, + }, + }, + v1beta1.Param{ + Name: "s2iImageScriptsUrl", + Value: v1beta1.ParamValue{ + Type: "string", + StringVal: s2iImageScriptsUrl, + }, + }, + }, + Workspaces: []v1beta1.WorkspaceBinding{ + v1beta1.WorkspaceBinding{ + Name: "source-workspace", + SubPath: "source", + PersistentVolumeClaim: &coreV1.PersistentVolumeClaimVolumeSource{ + ClaimName: getPipelinePvcName(f), + }, + }, + v1beta1.WorkspaceBinding{ + Name: "cache-workspace", + SubPath: "cache", + PersistentVolumeClaim: &coreV1.PersistentVolumeClaimVolumeSource{ + ClaimName: getPipelinePvcName(f), + }, + }, + v1beta1.WorkspaceBinding{ + Name: "dockerconfig-workspace", + Secret: &coreV1.SecretVolumeSource{ + SecretName: getPipelineSecretName(f), + }, + }, + }, + }, + } + return &result, nil +} + +type res interface { + GetGroupVersionKind() schema.GroupVersionKind + GetObjectKind() schema.ObjectKind +} + +func loadResource[T res](fileName string) (T, error) { + var result T + filePath := fileName + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + var file *os.File + file, err = os.Open(filePath) + if err != nil { + return result, fmt.Errorf("cannot opern resource file: %w", err) + } + defer file.Close() + dec := k8sYaml.NewYAMLToJSONDecoder(file) + err = dec.Decode(&result) + if err != nil { + return result, fmt.Errorf("cannot deserialize resource: %w", err) + } + gvk := result.GetGroupVersionKind() + if gvk != result.GetObjectKind().GroupVersionKind() { + return result, fmt.Errorf("unexpected resource type: %q", result.GetObjectKind().GroupVersionKind()) + } + return result, nil + } + return result, nil +} + func deletePipelines(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) { client, namespace, err := NewTektonClientAndResolvedNamespace(namespaceOverride) if err != nil { diff --git a/pkg/pipelines/tekton/task_defs.go b/pkg/pipelines/tekton/task_defs.go new file mode 100644 index 0000000000..3aae85e9ef --- /dev/null +++ b/pkg/pipelines/tekton/task_defs.go @@ -0,0 +1,597 @@ +package tekton + +import ( + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + BashImage = "docker.io/library/bash:5.1.4@sha256:b208215a4655538be652b2769d82e576bc4d0a2bb132144c060efc5be8c3f5d6" + S2IImage = "quay.io/boson/s2i:latest" + FuncImage = "ghcr.io/knative/func/func:latest" + BuildahImage = "quay.io/buildah/stable:v1.31.0" +) + +var BuildpackTask = v1beta1.Task{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Task", + APIVersion: "tekton.dev/v1beta1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "func-buildpacks", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }, + Annotations: map[string]string{ + "tekton.dev/displayName": "Knative Functions Buildpacks", + "tekton.dev/pipelines.minVersion": "0.17.0", + "tekton.dev/platforms": "linux/amd64", + "tekton.dev/tags": "image-build", + "tekton.dev/categories": "Image Build", + }, + }, + Spec: v1beta1.TaskSpec{ + Params: []v1beta1.ParamSpec{ + v1beta1.ParamSpec{ + Name: "APP_IMAGE", + Description: "The name of where to store the app image.", + }, + v1beta1.ParamSpec{ + Name: "REGISTRY", + Description: "The registry associated with the function image.", + }, + v1beta1.ParamSpec{ + Name: "BUILDER_IMAGE", + Description: "The image on which builds will run (must include lifecycle and compatible buildpacks).", + }, + v1beta1.ParamSpec{ + Name: "SOURCE_SUBPATH", + Description: "A subpath within the `source` input where the source to build is located.", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "ENV_VARS", + Type: "array", + Description: "Environment variables to set during _build-time_.", + Default: &v1beta1.ParamValue{ + Type: "array", + ArrayVal: []string{}, + }, + }, + v1beta1.ParamSpec{ + Name: "RUN_IMAGE", + Description: "Reference to a run image to use.", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "CACHE_IMAGE", + Description: "The name of the persistent app cache image (if no cache workspace is provided).", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "SKIP_RESTORE", + Description: "Do not write layer metadata or restore cached layers.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "false", + }, + }, + v1beta1.ParamSpec{ + Name: "USER_ID", + Description: "The user ID of the builder image user.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "1001", + }, + }, + v1beta1.ParamSpec{ + Name: "GROUP_ID", + Description: "The group ID of the builder image user.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "0", + }, + }, + v1beta1.ParamSpec{ + Name: "PLATFORM_DIR", + Description: "The name of the platform directory.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "empty-dir", + }, + }, + }, + Description: "The Knative Functions Buildpacks task builds source into a container image and pushes it to a registry, using Cloud Native Buildpacks. This task is based on the Buildpacks Tekton task v 0.4.", + Steps: []v1beta1.Step{ + v1beta1.Step{ + Name: "prepare", + Image: BashImage, + Args: []string{ + "--env-vars", + "$(params.ENV_VARS[*])", + }, + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "layers-dir", + MountPath: "/layers", + }, + v1.VolumeMount{ + Name: "$(params.PLATFORM_DIR)", + MountPath: "/platform", + }, + v1.VolumeMount{ + Name: "empty-dir", + MountPath: "/emptyDir", + }, + }, + Script: `#!/usr/bin/env bash +set -e + +if [[ "$(workspaces.cache.bound)" == "true" ]]; then + echo "> Setting permissions on '$(workspaces.cache.path)'..." + chown -R "$(params.USER_ID):$(params.GROUP_ID)" "$(workspaces.cache.path)" +fi + +####################################################### +##### "/emptyDir" has been added for Knative Functions +for path in "/tekton/home" "/layers" "/emptyDir" "$(workspaces.source.path)"; do + echo "> Setting permissions on '$path'..." + chown -R "$(params.USER_ID):$(params.GROUP_ID)" "$path" + + if [[ "$path" == "$(workspaces.source.path)" ]]; then + chmod 775 "$(workspaces.source.path)" + fi +done + +echo "> Parsing additional configuration..." +parsing_flag="" +envs=() +for arg in "$@"; do + if [[ "$arg" == "--env-vars" ]]; then + echo "-> Parsing env variables..." + parsing_flag="env-vars" + elif [[ "$parsing_flag" == "env-vars" ]]; then + envs+=("$arg") + fi +done + +echo "> Processing any environment variables..." +ENV_DIR="/platform/env" + +echo "--> Creating 'env' directory: $ENV_DIR" +mkdir -p "$ENV_DIR" + +for env in "${envs[@]}"; do + IFS='=' read -r key value <<< "$env" + if [[ "$key" != "" && "$value" != "" ]]; then + path="${ENV_DIR}/${key}" + echo "--> Writing ${path}..." + echo -n "$value" > "$path" + fi +done + +############################################ +##### Added part for Knative Functions ##### +############################################ + +func_file="$(workspaces.source.path)/func.yaml" +if [ "$(params.SOURCE_SUBPATH)" != "" ]; then + func_file="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)/func.yaml" +fi +echo "--> Saving 'func.yaml'" +cp $func_file /emptyDir/func.yaml + +############################################ +`, + }, + v1beta1.Step{ + Name: "create", + Image: "$(params.BUILDER_IMAGE)", + Command: []string{ + "/cnb/lifecycle/creator", + }, + Args: []string{ + "-app=$(workspaces.source.path)/$(params.SOURCE_SUBPATH)", + "-cache-dir=$(workspaces.cache.path)", + "-cache-image=$(params.CACHE_IMAGE)", + "-uid=$(params.USER_ID)", + "-gid=$(params.GROUP_ID)", + "-layers=/layers", + "-platform=/platform", + "-report=/layers/report.toml", + "-skip-restore=$(params.SKIP_RESTORE)", + "-previous-image=$(params.APP_IMAGE)", + "-run-image=$(params.RUN_IMAGE)", + "$(params.APP_IMAGE)", + }, + Env: []v1.EnvVar{ + v1.EnvVar{ + Name: "DOCKER_CONFIG", + Value: "$(workspaces.dockerconfig.path)", + }, + }, + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "layers-dir", + MountPath: "/layers", + }, + v1.VolumeMount{ + Name: "$(params.PLATFORM_DIR)", + MountPath: "/platform", + }, + }, + ImagePullPolicy: "Always", + SecurityContext: &v1.SecurityContext{ + RunAsUser: ptr(int64(1001)), + RunAsGroup: ptr(int64(0)), + }, + }, + v1beta1.Step{ + Name: "results", + Image: BashImage, + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "layers-dir", + MountPath: "/layers", + }, + v1.VolumeMount{ + Name: "empty-dir", + MountPath: "/emptyDir", + }, + }, + Script: `#!/usr/bin/env bash +set -e +cat /layers/report.toml | grep "digest" | cut -d'"' -f2 | cut -d'"' -f2 | tr -d '\n' | tee $(results.IMAGE_DIGEST.path) + +############################################ +##### Added part for Knative Functions ##### +############################################ + +digest=$(cat $(results.IMAGE_DIGEST.path)) + +func_file="$(workspaces.source.path)/func.yaml" +if [ "$(params.SOURCE_SUBPATH)" != "" ]; then + func_file="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)/func.yaml" +fi + +if [[ ! -f "$func_file" ]]; then + echo "--> Restoring 'func.yaml'" + mkdir -p "$(workspaces.source.path)/$(params.SOURCE_SUBPATH)" + cp /emptyDir/func.yaml $func_file +fi + +echo "" +sed -i "s|^image:.*$|image: $(params.APP_IMAGE)|" "$func_file" +echo "Function image name: $(params.APP_IMAGE)" + +sed -i "s/^imageDigest:.*$/imageDigest: $digest/" "$func_file" +echo "Function image digest: $digest" + +sed -i "s|^registry:.*$|registry: $(params.REGISTRY)|" "$func_file" +echo "Function image registry: $(params.REGISTRY)" + +############################################ +`, + }, + }, + Volumes: []v1.Volume{ + v1.Volume{ + Name: "empty-dir", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + v1.Volume{ + Name: "layers-dir", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + }, + StepTemplate: &v1beta1.StepTemplate{ + Env: []v1.EnvVar{ + v1.EnvVar{ + Name: "CNB_PLATFORM_API", + Value: "0.10", + }, + }, + }, + Workspaces: []v1beta1.WorkspaceDeclaration{ + v1beta1.WorkspaceDeclaration{ + Name: "source", + Description: "Directory where application source is located.", + }, + v1beta1.WorkspaceDeclaration{ + Name: "cache", + Description: "Directory where cache is stored (when no cache image is provided).", + Optional: true, + }, + v1beta1.WorkspaceDeclaration{ + Name: "dockerconfig", + Description: "An optional workspace that allows providing a .docker/config.json file for Buildpacks lifecycle binary to access the container registry. The file should be placed at the root of the Workspace with name config.json.", + Optional: true, + }, + }, + Results: []v1beta1.TaskResult{ + v1beta1.TaskResult{ + Name: "IMAGE_DIGEST", + Description: "The digest of the built `APP_IMAGE`.", + }, + }, + }, +} + +var S2ITask = v1beta1.Task{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Task", + APIVersion: "tekton.dev/v1beta1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "func-s2i", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }, + Annotations: map[string]string{ + "tekton.dev/pipelines.minVersion": "0.17.0", + "tekton.dev/platforms": "linux/amd64", + "tekton.dev/tags": "image-build", + "tekton.dev/categories": "Image Build", + }, + }, + Spec: v1beta1.TaskSpec{ + Params: []v1beta1.ParamSpec{ + v1beta1.ParamSpec{ + Name: "BUILDER_IMAGE", + Description: "The location of the s2i builder image.", + }, + v1beta1.ParamSpec{ + Name: "APP_IMAGE", + Description: "Reference of the image S2I will produce.", + }, + v1beta1.ParamSpec{ + Name: "REGISTRY", + Description: "The registry associated with the function image.", + }, + v1beta1.ParamSpec{ + Name: "SOURCE_SUBPATH", + Description: "The location of the path to run s2i from.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: ".", + }, + }, + v1beta1.ParamSpec{ + Name: "TLSVERIFY", + Description: "Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry)", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "true", + }, + }, + v1beta1.ParamSpec{ + Name: "LOGLEVEL", + Description: "Log level when running the S2I binary", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "0", + }, + }, + v1beta1.ParamSpec{ + Name: "ENV_VARS", + Type: "array", + Description: "Environment variables to set during _build-time_.", + Default: &v1beta1.ParamValue{ + Type: "array", + ArrayVal: []string{}, + }, + }, + v1beta1.ParamSpec{ + Name: "S2I_IMAGE_SCRIPTS_URL", + Description: "The URL containing the default assemble and run scripts for the builder image.", + Default: &v1beta1.ParamValue{ + Type: "string", + StringVal: "image:///usr/libexec/s2i", + }, + }, + }, + Description: `Knative Functions Source-to-Image (S2I) is a toolkit and workflow for building reproducible container images from source code +S2I produces images by injecting source code into a base S2I container image and letting the container prepare that source code for execution. The base S2I container images contains the language runtime and build tools needed for building and running the source code.`, + Steps: []v1beta1.Step{ + v1beta1.Step{ + Name: "generate", + Image: S2IImage, + Args: []string{ + "$(params.ENV_VARS[*])", + }, + WorkingDir: "$(workspaces.source.path)", + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "gen-source", + MountPath: "/gen-source", + }, + v1.VolumeMount{ + Name: "env-vars", + MountPath: "/env-vars", + }, + }, + Script: `echo "Processing Build Environment Variables" +echo "" > /env-vars/env-file +for var in "$@" +do + if [[ "$var" != "=" ]]; then + echo "$var" >> /env-vars/env-file + fi +done + +echo "Generated Build Env Var file" +echo "------------------------------" +cat /env-vars/env-file +echo "------------------------------" + +/usr/local/bin/s2i --loglevel=$(params.LOGLEVEL) build $(params.SOURCE_SUBPATH) $(params.BUILDER_IMAGE) \ +--image-scripts-url $(params.S2I_IMAGE_SCRIPTS_URL) \ +--as-dockerfile /gen-source/Dockerfile.gen --environment-file /env-vars/env-file + +echo "Preparing func.yaml for later deployment" +func_file="$(workspaces.source.path)/func.yaml" +if [ "$(params.SOURCE_SUBPATH)" != "" ]; then + func_file="$(workspaces.source.path)/$(params.SOURCE_SUBPATH)/func.yaml" +fi +sed -i "s|^registry:.*$|registry: $(params.REGISTRY)|" "$func_file" +echo "Function image registry: $(params.REGISTRY)" + +s2iignore_file="$(dirname "$func_file")/.s2iignore" +[ -f "$s2iignore_file" ] || echo "node_modules" >> "$s2iignore_file" +`, + }, + v1beta1.Step{ + Name: "build", + Image: BuildahImage, + WorkingDir: "/gen-source", + VolumeMounts: []v1.VolumeMount{ + v1.VolumeMount{ + Name: "varlibcontainers", + MountPath: "/var/lib/containers", + }, + v1.VolumeMount{ + Name: "gen-source", + MountPath: "/gen-source", + }, + }, + SecurityContext: &v1.SecurityContext{ + Capabilities: &v1.Capabilities{ + Add: []v1.Capability{ + "SETFCAP", + }, + }, + }, + Script: `TLS_VERIFY_FLAG="" +if [ "$(params.TLSVERIFY)" = "false" ] || [ "$(params.TLSVERIFY)" = "0" ]; then + TLS_VERIFY_FLAG="--tls-verify=false" +fi + +[[ "$(workspaces.sslcertdir.bound)" == "true" ]] && CERT_DIR_FLAG="--cert-dir $(workspaces.sslcertdir.path)" +ARTIFACTS_CACHE_PATH="$(workspaces.cache.path)/mvn-artifacts" +[ -d "${ARTIFACTS_CACHE_PATH}" ] || mkdir "${ARTIFACTS_CACHE_PATH}" +buildah ${CERT_DIR_FLAG} bud --storage-driver=vfs ${TLS_VERIFY_FLAG} --layers \ + -v "${ARTIFACTS_CACHE_PATH}:/tmp/artifacts/:rw,z,U" \ + -f /gen-source/Dockerfile.gen -t $(params.APP_IMAGE) . + +[[ "$(workspaces.dockerconfig.bound)" == "true" ]] && export DOCKER_CONFIG="$(workspaces.dockerconfig.path)" +buildah ${CERT_DIR_FLAG} push --storage-driver=vfs ${TLS_VERIFY_FLAG} --digestfile $(workspaces.source.path)/image-digest \ + $(params.APP_IMAGE) docker://$(params.APP_IMAGE) + +cat $(workspaces.source.path)/image-digest | tee /tekton/results/IMAGE_DIGEST +`, + }, + }, + Volumes: []v1.Volume{ + v1.Volume{ + Name: "varlibcontainers", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + v1.Volume{ + Name: "gen-source", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + v1.Volume{ + Name: "env-vars", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + }, + Workspaces: []v1beta1.WorkspaceDeclaration{ + v1beta1.WorkspaceDeclaration{ + Name: "source", + }, + v1beta1.WorkspaceDeclaration{ + Name: "cache", + Description: "Directory where cache is stored (e.g. local mvn repo).", + Optional: true, + }, + v1beta1.WorkspaceDeclaration{ + Name: "sslcertdir", + Optional: true, + }, + v1beta1.WorkspaceDeclaration{ + Name: "dockerconfig", + Description: "An optional workspace that allows providing a .docker/config.json file for Buildah to access the container registry. The file should be placed at the root of the Workspace with name config.json.", + Optional: true, + }, + }, + Results: []v1beta1.TaskResult{ + v1beta1.TaskResult{ + Name: "IMAGE_DIGEST", + Description: "Digest of the image just built.", + }, + }, + }, +} + +var DeployTask = v1beta1.Task{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Task", + APIVersion: "tekton.dev/v1beta1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "func-deploy", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }, + Annotations: map[string]string{ + "tekton.dev/pipelines.minVersion": "0.12.1", + "tekton.dev/platforms": "linux/amd64", + "tekton.dev/tags": "cli", + "tekton.dev/categories": "CLI", + }, + }, + Spec: v1beta1.TaskSpec{ + Params: []v1beta1.ParamSpec{ + v1beta1.ParamSpec{ + Name: "path", + Description: "Path to the function project", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + v1beta1.ParamSpec{ + Name: "image", + Description: "Container image to be deployed", + Default: &v1beta1.ParamValue{ + Type: "string", + }, + }, + }, + Description: "This Task performs a deploy operation using the Knative `func` CLI", + Steps: []v1beta1.Step{ + v1beta1.Step{ + Name: "func-deploy", + Image: FuncImage, + Script: `func deploy --verbose --build=false --push=false --path=$(params.path) --remote=false --image="$(params.image)" +`, + }, + }, + Workspaces: []v1beta1.WorkspaceDeclaration{ + v1beta1.WorkspaceDeclaration{ + Name: "source", + Description: "The workspace containing the function project", + }, + }, + }, +} + +func ptr[T any](val T) *T { + return &val +} diff --git a/pkg/pipelines/tekton/task_defs_test.go b/pkg/pipelines/tekton/task_defs_test.go new file mode 100644 index 0000000000..4f567eea32 --- /dev/null +++ b/pkg/pipelines/tekton/task_defs_test.go @@ -0,0 +1,51 @@ +package tekton_test + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + k8sYaml "k8s.io/apimachinery/pkg/util/yaml" + + "knative.dev/func/pkg/pipelines/tekton" +) + +func TestTaskMatch(t *testing.T) { + for _, tt := range []struct { + path string + task v1beta1.Task + }{ + { + path: "../resources/tekton/task/func-buildpacks/0.2/func-buildpacks.yaml", + task: tekton.BuildpackTask, + }, + { + path: "../resources/tekton/task/func-s2i/0.2/func-s2i.yaml", + task: tekton.S2ITask, + }, + { + path: "../resources/tekton/task/func-deploy/0.1/func-deploy.yaml", + task: tekton.DeployTask, + }, + } { + t.Run(tt.task.Name, func(t *testing.T) { + + f, err := os.Open(tt.path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + dec := k8sYaml.NewYAMLToJSONDecoder(f) + var taskFromYaml v1beta1.Task + err = dec.Decode(&taskFromYaml) + if err != nil { + t.Fatal(err) + } + if d := cmp.Diff(tt.task, taskFromYaml); d != "" { + t.Error("output missmatch (-want, +got):", d) + } + }) + } +} diff --git a/pkg/pipelines/tekton/templates.go b/pkg/pipelines/tekton/templates.go index b4f6f22eb6..6272f69bf7 100644 --- a/pkg/pipelines/tekton/templates.go +++ b/pkg/pipelines/tekton/templates.go @@ -1,21 +1,18 @@ package tekton import ( - "bytes" + "context" "fmt" - "net/http" "os" "path" - "strings" "text/template" "github.com/AlecAivazis/survey/v2" - "github.com/manifestival/manifestival" - "gopkg.in/yaml.v3" + "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/func/pkg/builders" fn "knative.dev/func/pkg/functions" - "knative.dev/func/pkg/k8s" ) const ( @@ -94,7 +91,7 @@ var ( taskBasePath = "https://raw.githubusercontent.com/" + FuncRepoRef + "/" + FuncRepoBranchRef + "/pkg/pipelines/resources/tekton/task/" BuildpackTaskURL = taskBasePath + "func-buildpacks/0.2/func-buildpacks.yaml" - S2ITaskURL = taskBasePath + "func-s2i/0.1/func-s2i.yaml" + S2ITaskURL = taskBasePath + "func-s2i/0.2/func-s2i.yaml" DeployTaskURL = taskBasePath + "func-deploy/0.1/func-deploy.yaml" ) @@ -286,209 +283,55 @@ func deleteAllPipelineTemplates(f fn.Function) string { return "" } -func getTaskSpec(taskUrlTemplate string) (string, error) { - resp, err := http.Get(taskUrlTemplate) - if err != nil { - return "", err - } - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("cannot get task: %q bad http code: %d", taskUrlTemplate, resp.StatusCode) - } - defer resp.Body.Close() - var data map[string]any - dec := yaml.NewDecoder(resp.Body) - err = dec.Decode(&data) - if err != nil { - return "", err - } - data = map[string]any{ - "taskSpec": data["spec"], - } - var buff bytes.Buffer - enc := yaml.NewEncoder(&buff) - enc.SetIndent(2) - err = enc.Encode(data) - if err != nil { - return "", err - } - err = enc.Close() - if err != nil { - return "", err - } - return strings.ReplaceAll(buff.String(), "\n", "\n "), nil -} - // createAndApplyPipelineTemplate creates and applies Pipeline template for a standard on-cluster build // all resources are created on the fly, if there's a Pipeline defined in the project directory, it is used instead func createAndApplyPipelineTemplate(f fn.Function, namespace string, labels map[string]string) error { - // If Git is set up create fetch task and reference it from build task, - // otherwise sources have been already uploaded to workspace PVC. - gitCloneTaskRef := "" - runAfterFetchSources := "" - if f.Build.Git.URL != "" { - runAfterFetchSources = runAfterFetchSourcesRef - gitCloneTaskRef = taskGitCloneTaskRef - } - - data := templateData{ - FunctionName: f.Name, - Annotations: f.Deploy.Annotations, - Labels: labels, - PipelineName: getPipelineName(f), - RunAfterFetchSources: runAfterFetchSources, - GitCloneTaskRef: gitCloneTaskRef, - } - - for _, val := range []struct { - ref string - field *string - }{ - {BuildpackTaskURL, &data.FuncBuildpacksTaskRef}, - {S2ITaskURL, &data.FuncS2iTaskRef}, - {DeployTaskURL, &data.FuncDeployTaskRef}, - } { - ts, err := getTaskSpec(val.ref) + if f.Build.Builder == builders.Pack || f.Build.Builder == builders.S2I { + iface, err := newTektonClient() if err != nil { - return err + return fmt.Errorf("cannot create tekton client: %w", err) } - *val.field = ts - } - - var template string - if f.Build.Builder == builders.Pack { - template = packPipelineTemplate - } else if f.Build.Builder == builders.S2I { - template = s2iPipelineTemplate + pipeline, err := getPipeline(f, labels) + if err != nil { + return fmt.Errorf("cannot generate pipeline: %w", err) + } + _, err = iface.TektonV1beta1().Pipelines(namespace).Create(context.TODO(), pipeline, v1.CreateOptions{}) + if err != nil { + err = fmt.Errorf("cannot create pipeline in cluster: %w", err) + } + return err } else { return builders.ErrBuilderNotSupported{Builder: f.Build.Builder} } - - return createAndApplyResource(f.Root, pipelineFileName, template, "pipeline", getPipelineName(f), namespace, data) } // createAndApplyPipelineRunTemplate creates and applies PipelineRun template for a standard on-cluster build // all resources are created on the fly, if there's a PipelineRun defined in the project directory, it is used instead func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels map[string]string) error { - contextDir := f.Build.Git.ContextDir - if contextDir == "" && f.Build.Builder == builders.S2I { - // TODO(lkingland): could instead update S2I to interpret empty string - // as cwd, such that builder-specific code can be kept out of here. - contextDir = "." - } - - pipelinesTargetBranch := f.Build.Git.Revision - if pipelinesTargetBranch == "" { - pipelinesTargetBranch = defaultPipelinesTargetBranch - } - - buildEnvs := []string{} - if len(f.Build.BuildEnvs) == 0 { - buildEnvs = []string{"="} - } else { - for i := range f.Build.BuildEnvs { - buildEnvs = append(buildEnvs, f.Build.BuildEnvs[i].KeyValuePair()) + if f.Build.Builder == builders.Pack || f.Build.Builder == builders.S2I { + iface, err := newTektonClient() + if err != nil { + return err } - } - - s2iImageScriptsUrl := defaultS2iImageScriptsUrl - if f.Runtime == "quarkus" { - s2iImageScriptsUrl = quarkusS2iImageScriptsUrl - } - - data := templateData{ - FunctionName: f.Name, - Annotations: f.Deploy.Annotations, - Labels: labels, - ContextDir: contextDir, - FunctionImage: f.Image, - Registry: f.Registry, - BuilderImage: getBuilderImage(f), - BuildEnvs: buildEnvs, - - PipelineName: getPipelineName(f), - PipelineRunName: getPipelineRunGenerateName(f), - PvcName: getPipelinePvcName(f), - SecretName: getPipelineSecretName(f), - - S2iImageScriptsUrl: s2iImageScriptsUrl, - - RepoUrl: f.Build.Git.URL, - Revision: pipelinesTargetBranch, - } - - var template string - if f.Build.Builder == builders.Pack { - template = packRunTemplate - } else if f.Build.Builder == builders.S2I { - template = s2iRunTemplate - } else { - return builders.ErrBuilderNotSupported{Builder: f.Build.Builder} - } - - return createAndApplyResource(f.Root, pipelineFileName, template, "pipelinerun", getPipelineRunGenerateName(f), namespace, data) -} - -// allows simple mocking in unit tests -var manifestivalClient = k8s.GetManifestivalClient - -// createAndApplyResource tries to create and apply a resource to the k8s cluster from the input template and data, -// if there's the same resource already created in the project directory, it is used instead -func createAndApplyResource(projectRoot, fileName, fileTemplate, kind, resourceName, namespace string, data interface{}) error { - var source manifestival.Source - - filePath := path.Join(projectRoot, resourcesDirectory, fileName) - if _, err := os.Stat(filePath); !os.IsNotExist(err) { - source = manifestival.Path(filePath) - } else { - tmpl, err := template.New("template").Parse(fileTemplate) + piplineRun, err := getPipelineRun(f, labels) if err != nil { - return fmt.Errorf("error parsing template: %v", err) + return fmt.Errorf("cannot generate pipeline run: %w", err) } - - var buf bytes.Buffer - err = tmpl.Execute(&buf, data) + _, err = iface.TektonV1beta1().PipelineRuns(namespace).Create(context.Background(), piplineRun, v1.CreateOptions{}) if err != nil { - return fmt.Errorf("error executing template: %v", err) + err = fmt.Errorf("cannot create pipeline run in cluster: %w", err) } - source = manifestival.Reader(&buf) - } - - client, err := manifestivalClient() - if err != nil { - return fmt.Errorf("error generating template: %v", err) - } - - m, err := manifestival.ManifestFrom(source, manifestival.UseClient(client)) - if err != nil { - return fmt.Errorf("error generating template: %v", err) - } - - resources := m.Resources() - if len(resources) != 1 { - return fmt.Errorf("error creating pipeline resources: there could be only a single resource in the template file %q", filePath) - } - - if strings.ToLower(resources[0].GetKind()) != kind { - return fmt.Errorf("error creating pipeline resources: expected resource kind in file %q is %q, but got %q", filePath, kind, resources[0].GetKind()) - } - - existingResourceName := resources[0].GetName() - if kind == "pipelinerun" { - existingResourceName = resources[0].GetGenerateName() - } - if existingResourceName != resourceName { - return fmt.Errorf("error creating pipeline resources: expected resource name in file %q is %q, but got %q", filePath, resourceName, existingResourceName) - } - - if resources[0].GetNamespace() != "" && resources[0].GetNamespace() != namespace { - return fmt.Errorf("error creating pipeline resources: expected resource namespace in file %q is %q, but got %q", filePath, namespace, resources[0].GetNamespace()) + return err + } else { + return builders.ErrBuilderNotSupported{Builder: f.Build.Builder} } +} - m, err = m.Transform(manifestival.InjectNamespace(namespace)) +// allows simple mocking in unit tests +var newTektonClient func() (versioned.Interface, error) = func() (versioned.Interface, error) { + cli, err := NewTektonClients() if err != nil { - fmt.Printf("error procesing template: %v", err) - return err + return nil, err } - - return m.Apply() + return cli.Tekton, nil } diff --git a/pkg/pipelines/tekton/templates_integration_test.go b/pkg/pipelines/tekton/templates_integration_test.go index ea6129adf8..49452a0a49 100644 --- a/pkg/pipelines/tekton/templates_integration_test.go +++ b/pkg/pipelines/tekton/templates_integration_test.go @@ -5,8 +5,8 @@ package tekton import ( "testing" - "github.com/manifestival/manifestival" - "github.com/manifestival/manifestival/fake" + "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + fakepipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" fn "knative.dev/func/pkg/functions" . "knative.dev/func/pkg/testing" @@ -16,11 +16,11 @@ func Test_createAndApplyPipelineTemplate(t *testing.T) { for _, tt := range testData { t.Run(tt.name, func(t *testing.T) { // save current function and restore it at the end - old := manifestivalClient - defer func() { manifestivalClient = old }() + old := newTektonClient + defer func() { newTektonClient = old }() - manifestivalClient = func() (manifestival.Client, error) { - return fake.New(), nil + newTektonClient = func() (versioned.Interface, error) { + return fakepipelineclientset.NewSimpleClientset(), nil } root := tt.root diff --git a/pkg/pipelines/tekton/templates_pack.go b/pkg/pipelines/tekton/templates_pack.go index 8dde6ded51..16c80e87f9 100644 --- a/pkg/pipelines/tekton/templates_pack.go +++ b/pkg/pipelines/tekton/templates_pack.go @@ -86,56 +86,6 @@ spec: optional: true ` - // packRunTemplate contains the Buildpacks template used for Tekton standard PipelineRun - packRunTemplate = ` -apiVersion: tekton.dev/v1beta1 -kind: PipelineRun -metadata: - labels: - {{range $key, $value := .Labels -}} - "{{$key}}": "{{$value}}" - {{end}} - tekton.dev/pipeline: {{.PipelineName}} - annotations: - # User defined Annotations - {{range $key, $value := .Annotations -}} - "{{$key}}": "{{$value}}" - {{end}} - generateName: {{.PipelineRunName}} -spec: - params: - - name: gitRepository - value: {{.RepoUrl}} - - name: gitRevision - value: {{.Revision}} - - name: contextDir - value: {{.ContextDir}} - - name: imageName - value: {{.FunctionImage}} - - name: registry - value: {{.Registry}} - - name: builderImage - value: {{.BuilderImage}} - - name: buildEnvs - value: - {{range .BuildEnvs -}} - - {{.}} - {{end}} - pipelineRef: - name: {{.PipelineName}} - workspaces: - - name: source-workspace - persistentVolumeClaim: - claimName: {{.PvcName}} - subPath: source - - name: cache-workspace - persistentVolumeClaim: - claimName: {{.PvcName}} - subPath: cache - - name: dockerconfig-workspace - secret: - secretName: {{.SecretName}} -` // packRunTemplatePAC contains the Buildpacks template used for the Tekton PAC PipelineRun packRunTemplatePAC = ` apiVersion: tekton.dev/v1beta1 diff --git a/pkg/pipelines/tekton/templates_s2i.go b/pkg/pipelines/tekton/templates_s2i.go index b78fc47a90..bc2e12e012 100644 --- a/pkg/pipelines/tekton/templates_s2i.go +++ b/pkg/pipelines/tekton/templates_s2i.go @@ -48,11 +48,11 @@ spec: {{.GitCloneTaskRef}} - name: build params: - - name: IMAGE + - name: APP_IMAGE value: $(params.imageName) - name: REGISTRY value: $(params.registry) - - name: PATH_CONTEXT + - name: SOURCE_SUBPATH value: $(params.contextDir) - name: BUILDER_IMAGE value: $(params.builderImage) @@ -91,58 +91,7 @@ spec: name: dockerconfig-workspace optional: true ` - // s2iRunTemplate contains the S2I template used for Tekton standard PipelineRun - s2iRunTemplate = ` -apiVersion: tekton.dev/v1beta1 -kind: PipelineRun -metadata: - labels: - {{range $key, $value := .Labels -}} - "{{$key}}": "{{$value}}" - {{end}} - tekton.dev/pipeline: {{.PipelineName}} - annotations: - # User defined Annotations - {{range $key, $value := .Annotations -}} - "{{$key}}": "{{$value}}" - {{end}} - generateName: {{.PipelineRunName}} -spec: - params: - - name: gitRepository - value: {{.RepoUrl}} - - name: gitRevision - value: {{.Revision}} - - name: contextDir - value: {{.ContextDir}} - - name: imageName - value: {{.FunctionImage}} - - name: registry - value: {{.Registry}} - - name: builderImage - value: {{.BuilderImage}} - - name: buildEnvs - value: - {{range .BuildEnvs -}} - - {{.}} - {{end}} - - name: s2iImageScriptsUrl - value: {{.S2iImageScriptsUrl}} - pipelineRef: - name: {{.PipelineName}} - workspaces: - - name: source-workspace - persistentVolumeClaim: - claimName: {{.PvcName}} - subPath: source - - name: cache-workspace - persistentVolumeClaim: - claimName: {{.PvcName}} - subPath: cache - - name: dockerconfig-workspace - secret: - secretName: {{.SecretName}} -` + // s2iRunTemplatePAC contains the S2I template used for Tekton PAC PipelineRun s2iRunTemplatePAC = ` apiVersion: tekton.dev/v1beta1 diff --git a/pkg/pipelines/tekton/templates_test.go b/pkg/pipelines/tekton/templates_test.go index 4fbc5e2863..2c25714813 100644 --- a/pkg/pipelines/tekton/templates_test.go +++ b/pkg/pipelines/tekton/templates_test.go @@ -4,8 +4,8 @@ import ( "path/filepath" "testing" - "github.com/manifestival/manifestival" - "github.com/manifestival/manifestival/fake" + "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + fakepipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" "knative.dev/func/pkg/builders" fn "knative.dev/func/pkg/functions" @@ -271,11 +271,11 @@ func Test_createAndApplyPipelineRunTemplate(t *testing.T) { for _, tt := range testData { t.Run(tt.name, func(t *testing.T) { // save current function and restore it at the end - old := manifestivalClient - defer func() { manifestivalClient = old }() + old := newTektonClient + defer func() { newTektonClient = old }() - manifestivalClient = func() (manifestival.Client, error) { - return fake.New(), nil + newTektonClient = func() (versioned.Interface, error) { + return fakepipelineclientset.NewSimpleClientset(), nil } root := tt.root + "Run"