func/pkg/pipelines/tekton/pipelines_pac_provider.go

263 lines
7.4 KiB
Go

package tekton
import (
"context"
"fmt"
"strings"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"knative.dev/func/pkg/docker"
fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/git"
"knative.dev/func/pkg/k8s"
"knative.dev/func/pkg/pipelines"
"knative.dev/func/pkg/pipelines/tekton/pac"
"knative.dev/func/pkg/random"
)
// ConfigurePAC cofigures Pipelines as Code resources based on the input:
// - locally (create .tekton directory)
// - on cluster (create Repository, Secret, PVC...)
// - on remote git repo (webhook)
// Parameter `metadata` is type `any` to not bring `pkg/pipelines` package dependency to `pkg/functions`,
// this specific implementation expects the parameter to be a type `pipelines.PacMetada`.
func (pp *PipelinesProvider) ConfigurePAC(ctx context.Context, f fn.Function, metadata any) error {
// Use the new target namespace if specified, otherwise use the
// function's currently deployed namespace (if any)
namespace := f.Namespace
if namespace == "" {
namespace = f.Deploy.Namespace
}
data, ok := metadata.(pipelines.PacMetadata)
if !ok {
return fmt.Errorf("incorrect type of pipelines metadata: %T", metadata)
}
var err error
if err = validatePipeline(f); err != nil {
return err
}
if data.ConfigureLocalResources {
if err := pp.createLocalPACResources(ctx, f); err != nil {
return err
}
}
if data.ConfigureClusterResources || data.ConfigureRemoteResources {
if data.WebhookSecret == "" {
data.WebhookSecret = random.AlphaString(10)
// try to reuse existing Webhook Secret stored in the cluster
secret, err := k8s.GetSecret(ctx, getPipelineSecretName(f), namespace)
if err != nil {
if !k8serrors.IsNotFound(err) {
return err
}
} else {
val, ok := secret.StringData["webhook.secret"]
if ok {
data.WebhookSecret = val
}
}
}
}
if data.ConfigureClusterResources {
if err := pp.createClusterPACResources(ctx, f, data); err != nil {
return err
}
}
if data.ConfigureRemoteResources {
if err := pp.createRemotePACResources(ctx, f, data); err != nil {
return err
}
}
return nil
}
// RemovePAC tries to remove all local and remote resources that were created for PAC.
// Resources on the remote GitHub repo are not removed, we would need to store webhook id somewhere locally.
func (pp *PipelinesProvider) RemovePAC(ctx context.Context, f fn.Function, metadata any) error {
data, ok := metadata.(pipelines.PacMetadata)
if !ok {
return fmt.Errorf("incorrect type of pipelines metadata: %T", metadata)
}
compoundErrMsg := ""
if data.ConfigureLocalResources {
errMsg := deleteAllPipelineTemplates(f)
compoundErrMsg += errMsg
}
if data.ConfigureClusterResources {
errMsg := pp.removeClusterResources(ctx, f)
if errMsg != nil {
compoundErrMsg += errMsg.Error()
}
}
if compoundErrMsg != "" {
return fmt.Errorf("%s", compoundErrMsg)
}
return nil
}
// createLocalPACResources creates necessary local resources in .tekton directory:
// Pipeline and PipelineRun templates
func (pp *PipelinesProvider) createLocalPACResources(ctx context.Context, f fn.Function) error {
// let's specify labels that will be applied to every resource that is created for a Pipeline
labels, err := f.LabelsMap()
if err != nil {
return err
}
if pp.decorator != nil {
labels = pp.decorator.UpdateLabels(f, labels)
}
err = createPipelineTemplatePAC(f, labels)
if err != nil {
return err
}
err = createPipelineRunTemplatePAC(f, labels)
if err != nil {
return err
}
return nil
}
// createClusterPACResources create resources on cluster, it tries to detect PAC installation,
// creates necessary secret with image registry credentials and git credentials (access tokens, webhook secrets),
// also creates PVC for the function source code
func (pp *PipelinesProvider) createClusterPACResources(ctx context.Context, f fn.Function, metadata pipelines.PacMetadata) error {
namespace := f.Namespace
if namespace == "" {
namespace = f.Deploy.Namespace
}
// figure out pac installation namespace
installed, _, err := pac.DetectPACInstallation(ctx)
if !installed {
errMsg := ""
if err != nil {
errMsg = fmt.Sprintf(", %v", err)
}
return fmt.Errorf("pipelines as code not installed%s", errMsg)
}
if installed && err != nil {
return err
}
// let's specify labels that will be applied to every resource that is created for a Pipeline
labels, err := f.LabelsMap()
if err != nil {
return err
}
if pp.decorator != nil {
labels = pp.decorator.UpdateLabels(f, labels)
}
img := f.Deploy.Image
if img == "" {
img = f.Image
}
registry, err := docker.GetRegistry(img)
if err != nil {
return fmt.Errorf("problem in resolving image registry name: %w", err)
}
if registry == name.DefaultRegistry {
registry = authn.DefaultAuthKey
}
creds, err := pp.credentialsProvider(ctx, img)
if err != nil {
return err
}
metadata.RegistryUsername = creds.Username
metadata.RegistryPassword = creds.Password
metadata.RegistryServer = registry
err = createPipelinePersistentVolumeClaim(ctx, f, namespace, labels)
if err != nil {
return err
}
fmt.Printf(" ✅ Persistent Volume is present on the cluster with name %q\n", getPipelinePvcName(f))
err = ensurePACSecretExists(ctx, f, namespace, metadata, labels)
if err != nil {
return err
}
fmt.Printf(" ✅ Credentials are present on the cluster in secret %q\n", getPipelineSecretName(f))
err = ensurePACRepositoryExists(ctx, f, namespace, metadata, labels)
if err != nil {
return err
}
fmt.Printf(" ✅ Webhook with payload validation secret %q is present on the cluster in repository %q\n", metadata.WebhookSecret, getPipelineRepositoryName(f))
return nil
}
// createRemotePACResources creates resources on the remote git repository
// set up a webhook with secrets, access tokens and it tries to detec PAC installation
// together with PAC controller route url - needed for webhook payload trigger
func (pp *PipelinesProvider) createRemotePACResources(ctx context.Context, f fn.Function, metadata pipelines.PacMetadata) error {
// figure out pac installation namespace
installed, installationNS, err := pac.DetectPACInstallation(ctx)
if !installed {
errMsg := ""
if err != nil {
errMsg = fmt.Sprintf(", %v", err)
}
return fmt.Errorf("pipelines as code not installed%s", errMsg)
}
if installed && err != nil {
return err
}
// fetch configmap to get controller url
controllerURL, err := pac.GetPACInfo(ctx, installationNS)
if err != nil {
return err
}
// check if info configmap has url then use that otherwise try to detect
if controllerURL == "" {
controllerURL, _ = pac.DetectPACOpenShiftRoute(ctx, installationNS)
}
// we haven't been able to detect PAC controller public route, let's prompt:
if controllerURL == "" {
if controllerURL, err = pp.getPacURL(); err != nil {
return err
}
}
if err := git.CreateWebHook(ctx, f.Build.Git.URL, controllerURL, metadata.WebhookSecret, metadata.PersonalAccessToken); err != nil {
// Error: POST https://api.github.com/repos/foobar/test-function/hooks: 422 Validation Failed [{Resource:Hook Field: Code:custom Message:Hook already exists on this repository}]
if !strings.Contains(err.Error(), "Hook already exists") {
return err
}
fmt.Printf(" ✅ Webhook already exists on repository %v\n", f.Build.Git.URL)
} else {
fmt.Printf(" ✅ Webhook is created on repository %v\n", f.Build.Git.URL)
}
return nil
}