mirror of https://github.com/knative/func.git
479 lines
14 KiB
Go
479 lines
14 KiB
Go
package tekton
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/manifestival/manifestival"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"knative.dev/func/pkg/builders"
|
|
fn "knative.dev/func/pkg/functions"
|
|
"knative.dev/func/pkg/k8s"
|
|
)
|
|
|
|
const (
|
|
// Local resources properties
|
|
resourcesDirectory = ".tekton"
|
|
pipelineFileName = "pipeline.yaml"
|
|
pipelineRunFilenane = "pipeline-run.yaml"
|
|
pipelineFileNamePAC = "pipeline-pac.yaml"
|
|
pipelineRunFilenamePAC = "pipeline-run-pac.yaml"
|
|
|
|
// Tasks references for PAC PipelineRun that are defined in the annotations
|
|
taskGitCloneRef = "git-clone"
|
|
|
|
// Following part holds a reference to Git Clone Task to be used in Pipeline template,
|
|
// the usage depends whether we use direct code upload or Git reference for a standard (non PAC) on-cluster build
|
|
taskGitClonePACTaskRef = `- name: fetch-sources
|
|
params:
|
|
- name: url
|
|
value: $(params.gitRepository)
|
|
- name: revision
|
|
value: $(params.gitRevision)
|
|
taskRef:
|
|
kind: Task
|
|
name: git-clone
|
|
workspaces:
|
|
- name: output
|
|
workspace: source-workspace`
|
|
// TODO fix Tekton Hub reference
|
|
taskGitCloneTaskRef = `- name: fetch-sources
|
|
params:
|
|
- name: url
|
|
value: $(params.gitRepository)
|
|
- name: revision
|
|
value: $(params.gitRevision)
|
|
taskRef:
|
|
resolver: hub
|
|
params:
|
|
- name: kind
|
|
value: task
|
|
- name: name
|
|
value: git-clone
|
|
- name: version
|
|
value: "0.4"
|
|
workspaces:
|
|
- name: output
|
|
workspace: source-workspace`
|
|
runAfterFetchSourcesRef = `runAfter:
|
|
- fetch-sources`
|
|
|
|
// S2I related properties
|
|
defaultS2iImageScriptsUrl = "image:///usr/libexec/s2i"
|
|
quarkusS2iImageScriptsUrl = "image:///usr/local/s2i"
|
|
|
|
// The branch or tag we are targeting with Pipelines (ie: main, refs/tags/*)
|
|
defaultPipelinesTargetBranch = "main"
|
|
)
|
|
|
|
type templateData struct {
|
|
FunctionName string
|
|
Annotations map[string]string
|
|
Labels map[string]string
|
|
ContextDir string
|
|
FunctionImage string
|
|
Registry string
|
|
BuilderImage string
|
|
BuildEnvs []string
|
|
|
|
PipelineName string
|
|
PipelineRunName string
|
|
PvcName string
|
|
SecretName string
|
|
|
|
// The branch or tag we are targeting with Pipelines (ie: main, refs/tags/*)
|
|
PipelinesTargetBranch string
|
|
|
|
// Static entries
|
|
RepoUrl string
|
|
Revision string
|
|
|
|
// Task references
|
|
GitCloneTaskRef string
|
|
FuncBuildpacksTaskRef string
|
|
FuncS2iTaskRef string
|
|
FuncDeployTaskRef string
|
|
FuncScaffoldTaskRef string
|
|
|
|
// Reference for build task - whether it should run after fetch-sources task or not
|
|
RunAfterFetchSources string
|
|
|
|
PipelineYamlURL string
|
|
|
|
// S2I related properties
|
|
S2iImageScriptsUrl string
|
|
}
|
|
|
|
// createPipelineTemplatePAC creates a Pipeline template used for PAC on-cluster build
|
|
// it creates the resource in the project directory
|
|
func createPipelineTemplatePAC(f fn.Function, labels map[string]string) error {
|
|
data := templateData{
|
|
FunctionName: f.Name,
|
|
Annotations: f.Deploy.Annotations,
|
|
Labels: labels,
|
|
PipelineName: getPipelineName(f),
|
|
RunAfterFetchSources: runAfterFetchSourcesRef,
|
|
GitCloneTaskRef: taskGitClonePACTaskRef,
|
|
}
|
|
|
|
for _, val := range []struct {
|
|
ref string
|
|
field *string
|
|
}{
|
|
{getBuildpackTask(), &data.FuncBuildpacksTaskRef},
|
|
{getS2ITask(), &data.FuncS2iTaskRef},
|
|
{getDeployTask(), &data.FuncDeployTaskRef},
|
|
{getScaffoldTask(), &data.FuncScaffoldTaskRef},
|
|
} {
|
|
ts, err := getTaskSpec(val.ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*val.field = ts
|
|
}
|
|
|
|
var template string
|
|
if f.Build.Builder == builders.Pack {
|
|
template = packPipelineTemplate
|
|
} else if f.Build.Builder == builders.S2I {
|
|
template = s2iPipelineTemplate
|
|
} else {
|
|
return builders.ErrBuilderNotSupported{Builder: f.Build.Builder}
|
|
}
|
|
|
|
return createResource(f.Root, pipelineFileNamePAC, template, data)
|
|
}
|
|
|
|
// createPipelineRunTemplatePAC creates a PipelineRun template used for PAC on-cluster build
|
|
// it creates the resource in the project directory
|
|
func createPipelineRunTemplatePAC(f fn.Function, 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())
|
|
}
|
|
}
|
|
|
|
s2iImageScriptsUrl := defaultS2iImageScriptsUrl
|
|
if f.Runtime == "quarkus" {
|
|
s2iImageScriptsUrl = quarkusS2iImageScriptsUrl
|
|
}
|
|
|
|
image := f.Deploy.Image
|
|
if image == "" {
|
|
image = f.Image
|
|
}
|
|
|
|
data := templateData{
|
|
FunctionName: f.Name,
|
|
Annotations: f.Deploy.Annotations,
|
|
Labels: labels,
|
|
ContextDir: contextDir,
|
|
FunctionImage: image,
|
|
Registry: f.Registry,
|
|
BuilderImage: getBuilderImage(f),
|
|
BuildEnvs: buildEnvs,
|
|
|
|
PipelineName: getPipelineName(f),
|
|
PipelineRunName: fmt.Sprintf("%s-run", getPipelineName(f)),
|
|
PvcName: getPipelinePvcName(f),
|
|
SecretName: getPipelineSecretName(f),
|
|
|
|
PipelinesTargetBranch: pipelinesTargetBranch,
|
|
|
|
GitCloneTaskRef: taskGitCloneRef,
|
|
|
|
PipelineYamlURL: fmt.Sprintf("%s/%s", resourcesDirectory, pipelineFileNamePAC),
|
|
|
|
S2iImageScriptsUrl: s2iImageScriptsUrl,
|
|
|
|
RepoUrl: "\"{{ repo_url }}\"",
|
|
Revision: "\"{{ revision }}\"",
|
|
}
|
|
|
|
var template string
|
|
if f.Build.Builder == builders.Pack {
|
|
template = packRunTemplatePAC
|
|
} else if f.Build.Builder == builders.S2I {
|
|
template = s2iRunTemplatePAC
|
|
} else {
|
|
return builders.ErrBuilderNotSupported{Builder: f.Build.Builder}
|
|
}
|
|
|
|
return createResource(f.Root, pipelineRunFilenamePAC, template, data)
|
|
}
|
|
|
|
// createResource creates a file in the input directory from the file template and a data
|
|
func createResource(projectRoot, fileName, fileTemplate string, data interface{}) error {
|
|
tmpl, err := template.New(fileName).Parse(fileTemplate)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing pipeline template: %v", err)
|
|
}
|
|
|
|
if err = os.MkdirAll(path.Join(projectRoot, resourcesDirectory), os.ModePerm); err != nil {
|
|
return fmt.Errorf("error creating pipeline resources path: %v", err)
|
|
}
|
|
|
|
filePath := path.Join(projectRoot, resourcesDirectory, fileName)
|
|
|
|
overwrite := false
|
|
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
|
|
msg := fmt.Sprintf("There is already a file %q in the %q directory, do you want to overwrite it?", fileName, resourcesDirectory)
|
|
if err := survey.AskOne(&survey.Confirm{Message: msg, Default: true}, &overwrite); err != nil {
|
|
return err
|
|
}
|
|
if !overwrite {
|
|
fmt.Printf(" ⚠️ Pipeline template is not updated in \"%s/%s\"\n", resourcesDirectory, fileName)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
file, err := os.Create(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating pipeline resources: %v", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
err = tmpl.Execute(file, data)
|
|
if err == nil {
|
|
if overwrite {
|
|
fmt.Printf(" ✅ Pipeline template is updated in \"%s/%s\"\n", resourcesDirectory, fileName)
|
|
} else {
|
|
fmt.Printf(" ✅ Pipeline template is created in \"%s/%s\"\n", resourcesDirectory, fileName)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// deleteAllPipelineTemplates deletes all templates and pipeline resources that exists for a function
|
|
// and are stored in the .tekton directory
|
|
func deleteAllPipelineTemplates(f fn.Function) string {
|
|
err := os.RemoveAll(path.Join(f.Root, resourcesDirectory))
|
|
if err != nil {
|
|
return fmt.Sprintf("\n %v", err)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func getTaskSpec(taskYaml string) (string, error) {
|
|
var err error
|
|
var data map[string]any
|
|
dec := yaml.NewDecoder(strings.NewReader(taskYaml))
|
|
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
|
|
}{
|
|
{getBuildpackTask(), &data.FuncBuildpacksTaskRef},
|
|
{getS2ITask(), &data.FuncS2iTaskRef},
|
|
{getDeployTask(), &data.FuncDeployTaskRef},
|
|
{getScaffoldTask(), &data.FuncScaffoldTaskRef},
|
|
} {
|
|
ts, err := getTaskSpec(val.ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*val.field = ts
|
|
}
|
|
|
|
var template string
|
|
if f.Build.Builder == builders.Pack {
|
|
template = packPipelineTemplate
|
|
} else if f.Build.Builder == builders.S2I {
|
|
template = s2iPipelineTemplate
|
|
} 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())
|
|
}
|
|
}
|
|
|
|
s2iImageScriptsUrl := defaultS2iImageScriptsUrl
|
|
if f.Runtime == "quarkus" {
|
|
s2iImageScriptsUrl = quarkusS2iImageScriptsUrl
|
|
}
|
|
|
|
data := templateData{
|
|
FunctionName: f.Name,
|
|
Annotations: f.Deploy.Annotations,
|
|
Labels: labels,
|
|
ContextDir: contextDir,
|
|
FunctionImage: f.Deploy.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)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing template: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = tmpl.Execute(&buf, data)
|
|
if err != nil {
|
|
return fmt.Errorf("error executing template: %v", 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())
|
|
}
|
|
|
|
m, err = m.Transform(manifestival.InjectNamespace(namespace))
|
|
if err != nil {
|
|
fmt.Printf("error procesing template: %v", err)
|
|
return err
|
|
}
|
|
|
|
return m.Apply()
|
|
}
|