//go:build integration // +build integration package tekton_test import ( "context" "fmt" "io" "net/http" "net/http/httputil" "os" "os/signal" "path/filepath" "strconv" "strings" "testing" "time" corev1 "k8s.io/api/core/v1" rbacV1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" "knative.dev/func/pkg/builders/buildpacks" pack "knative.dev/func/pkg/builders/buildpacks" "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/pipelines/tekton" "knative.dev/func/pkg/random" . "knative.dev/func/pkg/testing" ) var testCP = func(_ context.Context, _ string) (docker.Credentials, error) { return docker.Credentials{ Username: "", Password: "", }, nil } const ( TestRegistry = "registry.default.svc.cluster.local:5000" // TestRegistry = "docker.io/alice" TestNamespace = "default" ) func newRemoteTestClient(verbose bool) *fn.Client { return fn.New( fn.WithBuilder(pack.NewBuilder(pack.WithVerbose(verbose))), fn.WithPusher(docker.NewPusher(docker.WithCredentialsProvider(testCP))), fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose))), fn.WithRemover(knative.NewRemover(verbose)), fn.WithDescriber(knative.NewDescriber(verbose)), fn.WithRemover(knative.NewRemover(verbose)), fn.WithPipelinesProvider(tekton.NewPipelinesProvider(tekton.WithCredentialsProvider(testCP), tekton.WithVerbose(verbose))), ) } // assertFunctionEchoes returns without error when the function of the given // name echoes a parameter sent via a Get request. func assertFunctionEchoes(url string) (err error) { token := time.Now().Format("20060102150405.000000000") // res, err := http.Get("http://testremote-default.default.127.0.0.1.sslip.io?token=" + token) res, err := http.Get(url + "?token=" + token) if err != nil { return } if res.StatusCode != 200 { return fmt.Errorf("unexpected status code %v", res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { return fmt.Errorf("error parsing response. %w", err) } defer res.Body.Close() if !strings.Contains(string(body), token) { err = fmt.Errorf("response did not contain token. url: %v", url) _, _ = httputil.DumpResponse(res, true) } return } func tektonTestsEnabled(t *testing.T) (enabled bool) { enabled, _ = strconv.ParseBool(os.Getenv("TEKTON_TESTS_ENABLED")) if !enabled { t.Log("Tekton tests not enabled. Enable with TEKTON_TESTS_ENABLED=true") } return } // fromCleanEnvironment of everything except KUBECONFIG. Create a temp directory. // Change to that temp directory. Return the curent path as a convenience. func fromCleanEnvironment(t *testing.T) (root string) { // FromTempDirectory clears envs, but sets KUBECONFIG to ./tempdata, so // we have to preserve that one value. t.Helper() kubeconfig := os.Getenv("KUBECONFIG") root = FromTempDirectory(t) os.Setenv("KUBECONFIG", kubeconfig) return } func TestRemote_Default(t *testing.T) { if !tektonTestsEnabled(t) { t.Skip() } _ = fromCleanEnvironment(t) var ( err error url string verbose = false ctx, cancel = signal.NotifyContext(context.Background(), os.Interrupt) client = newRemoteTestClient(verbose) ) defer cancel() f := fn.Function{ Name: "testremote-default", Runtime: "node", Registry: TestRegistry, Namespace: TestNamespace, Build: fn.BuildSpec{ Builder: "pack", // TODO: test "s2i". Currently it causes a 'no space left on device' error in GH actions. }, } if f, err = client.Init(f); err != nil { t.Fatal(err) } if url, f, err = client.RunPipeline(ctx, f); err != nil { t.Fatal(err) } defer func() { _ = client.Remove(ctx, "", "", f, true) }() if err := assertFunctionEchoes(url); err != nil { t.Fatal(err) } } func setupNS(t *testing.T) string { name := "pipeline-integration-test-" + strings.ToLower(random.AlphaString(5)) cliSet, err := k8s.NewKubernetesClientset() if err != nil { t.Fatal(err) } ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, } _, err = cliSet.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{}) if err != nil && !errors.IsAlreadyExists(err) { t.Fatal(err) } t.Cleanup(func() { pp := metav1.DeletePropagationForeground _ = cliSet.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{ PropagationPolicy: &pp, }) }) crb := &rbacV1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: name + ":knative-serving-namespaced-admin", }, Subjects: []rbacV1.Subject{ { Kind: "ServiceAccount", Name: "default", Namespace: name, }, }, RoleRef: rbacV1.RoleRef{ Name: "knative-serving-namespaced-admin", Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", }, } _, err = cliSet.RbacV1().ClusterRoleBindings().Create(context.Background(), crb, metav1.CreateOptions{}) if err != nil { t.Fatal(err) } t.Log("created namespace: ", name) return name } func checkTestEnabled(t *testing.T) { val := os.Getenv("TEKTON_TESTS_ENABLED") enabled, _ := strconv.ParseBool(val) if !enabled { t.Skip("tekton tests are not enabled") } } func createSimpleGoProject(t *testing.T, ns string) fn.Function { var err error funcName := "fn-" + strings.ToLower(random.AlphaString(5)) projDir := filepath.Join(t.TempDir(), funcName) err = os.Mkdir(projDir, 0755) if err != nil { t.Fatal(err) } err = os.WriteFile(filepath.Join(projDir, "handle.go"), []byte(simpleGOSvc), 0644) if err != nil { t.Fatal(err) } err = os.WriteFile(filepath.Join(projDir, "go.mod"), []byte("module function\n\ngo 1.20\n"), 0644) if err != nil { t.Fatal(err) } f := fn.Function{ Root: projDir, Name: funcName, Runtime: "none", Template: "none", Image: "registry.default.svc.cluster.local:5000/" + funcName, Created: time.Now(), Invoke: "none", Build: fn.BuildSpec{ BuilderImages: map[string]string{ "pack": buildpacks.DefaultTinyBuilder, "s2i": "registry.access.redhat.com/ubi8/go-toolset", }, }, Deploy: fn.DeploySpec{ Namespace: ns, }, } f = fn.NewFunctionWith(f) err = f.Write() if err != nil { t.Fatal(err) } return f } const simpleGOSvc = `package function import ( "context" "net/http" ) 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")) } `