diff --git a/pkg/builders/builders_int_test.go b/pkg/builders/builders_int_test.go new file mode 100644 index 00000000..408e7023 --- /dev/null +++ b/pkg/builders/builders_int_test.go @@ -0,0 +1,381 @@ +package builders_test + +import ( + "archive/tar" + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "os/signal" + "path/filepath" + "syscall" + "testing" + "time" + + "golang.org/x/term" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + coreV1 "k8s.io/api/core/v1" + v1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "knative.dev/func/pkg/builders/buildpacks" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" +) + +func TestPrivateGitRepository(t *testing.T) { + t.Skip("tested functionality not implemented yet") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + cancel() + <-sigs // second sigint/sigterm is treated as sigkill + os.Exit(137) + }() + + certDir := createCertificate(t) + t.Log("certDir:", certDir) + + builderImage := buildPatchedBuilder(ctx, t, certDir) + t.Log("builder image:", builderImage) + + servePrivateGit(ctx, t, certDir) + t.Log("git server initiated") + + select { + case <-time.After(time.Second * 5): + break + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + builder := buildpacks.NewBuilder(buildpacks.WithVerbose(true)) + f, err := fn.NewFunction(filepath.Join("testdata", "go-fn-with-private-deps")) + if err != nil { + t.Fatal(err) + } + f.Build.Image = "localhost:50000/go-app:test" + f.Build.Builder = "pack" + f.Build.BuilderImages = map[string]string{"pack": builderImage} + err = builder.Build(ctx, f, nil) + if err != nil { + t.Fatal(err) + } +} + +// Generates self-signed certificate used for our private git repository. +func createCertificate(t *testing.T) string { + dir := t.TempDir() + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + ski := sha1.Sum(x509.MarshalPKCS1PublicKey(&certPrivKey.PublicKey)) + + cert := &x509.Certificate{ + SerialNumber: randSN(), + // openssl hash of this subject is 712d4c9d + // do not update the subject without also updating the hash referred from another places (e.g. Dockerfile) + // See also: https://github.com/paketo-buildpacks/ca-certificates/blob/v1.0.1/cacerts/certs.go#L132 + Subject: pkix.Name{ + CommonName: "git-private.127.0.0.1.sslip.io", + }, + DNSNames: []string{"git-private.127.0.0.1.sslip.io"}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(0, 0, 1), + SubjectKeyId: ski[:], + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey) + if err != nil { + t.Fatal(err) + } + + certPEM := new(bytes.Buffer) + err = pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + t.Fatal(err) + } + + certPrivKeyPEM := new(bytes.Buffer) + err = pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(filepath.Join(dir, "cert.pem"), certPEM.Bytes(), 0444) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile(filepath.Join(dir, "key.pem"), certPrivKeyPEM.Bytes(), 0400) + if err != nil { + t.Fatal(err) + } + + return dir +} + +var maxSN *big.Int = new(big.Int).Lsh(big.NewInt(1), 159) + +func randSN() *big.Int { + i, err := rand.Int(rand.Reader, maxSN) + if err != nil { + panic(err) + } + return i +} + +// Builds a tiny paketo builder that trusts to our self-signed certificate (see createCertificate). +func buildPatchedBuilder(ctx context.Context, t *testing.T, certDir string) string { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + t.Fatal(err) + } + + dockerfile := `FROM ghcr.io/knative/builder-jammy-base:latest +COPY 712d4c9d.0 /etc/ssl/certs/ +` + + var buff bytes.Buffer + tw := tar.NewWriter(&buff) + + err = tw.WriteHeader(&tar.Header{ + Name: "Dockerfile", + Size: int64(len(dockerfile)), + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + _, err = tw.Write([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + + cb, err := os.ReadFile(filepath.Join(certDir, "cert.pem")) + if err != nil { + t.Fatal(err) + } + err = tw.WriteHeader(&tar.Header{ + Name: "712d4c9d.0", + Size: int64(len(cb)), + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + _, err = tw.Write(cb) + if err != nil { + t.Fatal(err) + } + + err = tw.Close() + if err != nil { + t.Fatal(err) + } + + tag := "localhost:50000/tiny-builder:test" + ibo := types.ImageBuildOptions{ + Tags: []string{tag}, + } + ibr, err := cli.ImageBuild(ctx, &buff, ibo) + if err != nil { + t.Fatal(err) + } + defer ibr.Body.Close() + + fd := os.Stderr.Fd() + isTerminal := term.IsTerminal(int(fd)) + err = jsonmessage.DisplayJSONMessagesStream(ibr.Body, os.Stderr, fd, isTerminal, nil) + if err != nil { + t.Fatal(err) + } + + rc, err := cli.ImagePush(ctx, tag, image.PushOptions{RegistryAuth: "e30="}) + if err != nil { + t.Fatal(err) + } + defer rc.Close() + + return tag +} + +// This sets up a private git repository for testing. +// The repository url is https://git-private.127.0.0.1.sslip.io/foo.git, and it is protected by basic authentication. +// The credentials are developer:nbusr123. +func servePrivateGit(ctx context.Context, t *testing.T, certDir string) { + const ( + name = "git-private" + host = "git-private.127.0.0.1.sslip.io" + image = "ghcr.io/matejvasek/git-private:latest" + ) + + k8sClient, err := k8s.NewKubernetesClientset() + if err != nil { + t.Fatal(err) + } + + ns := coreV1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + _, err = k8sClient.CoreV1().Namespaces().Create(ctx, &ns, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = k8sClient.CoreV1().Namespaces().Delete(context.Background(), name, metav1.DeleteOptions{}) + }) + + cert, err := os.ReadFile(filepath.Join(certDir, "cert.pem")) + if err != nil { + t.Fatal(err) + } + key, err := os.ReadFile(filepath.Join(certDir, "key.pem")) + if err != nil { + t.Fatal(err) + } + + secret := coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: name, + }, + Immutable: ptr(true), + Data: map[string][]byte{ + coreV1.TLSCertKey: cert, + coreV1.TLSPrivateKeyKey: key, + }, + Type: coreV1.SecretTypeTLS, + } + + _, err = k8sClient.CoreV1().Secrets(name).Create(ctx, &secret, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + pod := coreV1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: name, + Labels: map[string]string{"app.kubernetes.io/name": name}, + }, + Spec: coreV1.PodSpec{ + Containers: []coreV1.Container{ + { + Name: name, + Image: image, + Ports: []coreV1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + }, + }, + }, + }, + }, + } + _, err = k8sClient.CoreV1().Pods(name).Create(ctx, &pod, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + svc := coreV1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: name, + }, + Spec: coreV1.ServiceSpec{ + Selector: map[string]string{ + "app.kubernetes.io/name": name, + }, + Ports: []coreV1.ServicePort{ + { + Name: "http", + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromString("http"), + }, + }, + Type: coreV1.ServiceTypeClusterIP, + }, + } + _, err = k8sClient.CoreV1().Services(name).Create(ctx, &svc, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + ingress := v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: name, + }, + Spec: v1.IngressSpec{ + IngressClassName: ptr("contour-external"), + DefaultBackend: nil, + TLS: []v1.IngressTLS{ + { + Hosts: []string{host}, + SecretName: name, + }, + }, + Rules: []v1.IngressRule{ + { + Host: host, + IngressRuleValue: v1.IngressRuleValue{ + HTTP: &v1.HTTPIngressRuleValue{Paths: []v1.HTTPIngressPath{ + { + Path: "/", + PathType: ptr(v1.PathTypePrefix), + Backend: v1.IngressBackend{ + Service: &v1.IngressServiceBackend{ + Name: name, + Port: v1.ServiceBackendPort{ + Name: "http", + }, + }, + }, + }, + }}, + }, + }, + }, + }, + } + _, err = k8sClient.NetworkingV1().Ingresses(name).Create(ctx, &ingress, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } +} + +func ptr[T any](val T) *T { + return &val +} diff --git a/pkg/builders/testdata/go-fn-with-private-deps/.funcignore b/pkg/builders/testdata/go-fn-with-private-deps/.funcignore new file mode 100644 index 00000000..e8e281c6 --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/.funcignore @@ -0,0 +1,5 @@ + +# Use the .funcignore file to exclude files which should not be +# tracked in the image build. To instruct the system not to track +# files in the image build, add the regex pattern or file information +# to this file. diff --git a/pkg/builders/testdata/go-fn-with-private-deps/.gitignore b/pkg/builders/testdata/go-fn-with-private-deps/.gitignore new file mode 100644 index 00000000..965f0d4e --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/.gitignore @@ -0,0 +1,5 @@ + +# Functions use the .func directory for local runtime data which should +# generally not be tracked in source control. To instruct the system to track +# .func in source control, comment the following line (prefix it with '# '). +/.func diff --git a/pkg/builders/testdata/go-fn-with-private-deps/README.md b/pkg/builders/testdata/go-fn-with-private-deps/README.md new file mode 100644 index 00000000..19e8258d --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/README.md @@ -0,0 +1,20 @@ +# Go HTTP Function + +Welcome to your new Go Function! The boilerplate function code can be found in +[`handle.go`](handle.go). This Function responds to HTTP requests. + +## Development + +Develop new features by adding a test to [`handle_test.go`](handle_test.go) for +each feature, and confirm it works with `go test`. + +Update the running analog of the function using the `func` CLI or client +library, and it can be invoked from your browser or from the command line: + +```console +curl http://myfunction.example.com/ +``` + +For more, see [the complete documentation]('https://github.com/knative/func/tree/main/docs') + + diff --git a/pkg/builders/testdata/go-fn-with-private-deps/func.yaml b/pkg/builders/testdata/go-fn-with-private-deps/func.yaml new file mode 100644 index 00000000..b4ad53b9 --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/func.yaml @@ -0,0 +1,4 @@ +specVersion: 0.36.0 +name: go-fn +runtime: go +created: 2025-03-17T02:02:34.196208671+01:00 diff --git a/pkg/builders/testdata/go-fn-with-private-deps/go.mod b/pkg/builders/testdata/go-fn-with-private-deps/go.mod new file mode 100644 index 00000000..8888b399 --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/go.mod @@ -0,0 +1,5 @@ +module function + +go 1.23.4 + +require git-private.127.0.0.1.sslip.io/foo.git v0.0.0-20250312185939-e7bf19abfd77 // indirect diff --git a/pkg/builders/testdata/go-fn-with-private-deps/go.sum b/pkg/builders/testdata/go-fn-with-private-deps/go.sum new file mode 100644 index 00000000..bb26fb82 --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/go.sum @@ -0,0 +1,2 @@ +git-private.127.0.0.1.sslip.io/foo.git v0.0.0-20250312185939-e7bf19abfd77 h1:IJ6SiucMsd0bjPwuj3VIGCHN225+0lCU1OZSmsg3Rjk= +git-private.127.0.0.1.sslip.io/foo.git v0.0.0-20250312185939-e7bf19abfd77/go.mod h1:rG9yzCHOSu2zUBmaB7GEu7RBsjiJZRUP1hy26O5CLsc= diff --git a/pkg/builders/testdata/go-fn-with-private-deps/handle.go b/pkg/builders/testdata/go-fn-with-private-deps/handle.go new file mode 100644 index 00000000..d2ac8d4e --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/handle.go @@ -0,0 +1,15 @@ +package function + +import ( + "fmt" + "net/http" + + "git-private.127.0.0.1.sslip.io/foo.git/pkg/foo" +) + +// Handle an HTTP Request. +func Handle(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain") + w.WriteHeader(200) + _, _ = fmt.Fprintf(w, "The answer is: %d", foo.Foo()) +} diff --git a/pkg/builders/testdata/go-fn-with-private-deps/handle_test.go b/pkg/builders/testdata/go-fn-with-private-deps/handle_test.go new file mode 100644 index 00000000..06de326e --- /dev/null +++ b/pkg/builders/testdata/go-fn-with-private-deps/handle_test.go @@ -0,0 +1,25 @@ +package function + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// TestHandle ensures that Handle executes without error and returns the +// HTTP 200 status code indicating no errors. +func TestHandle(t *testing.T) { + var ( + w = httptest.NewRecorder() + req = httptest.NewRequest("GET", "http://example.com/test", nil) + res *http.Response + ) + + Handle(w, req) + res = w.Result() + defer res.Body.Close() + + if res.StatusCode != 200 { + t.Fatalf("unexpected response code: %v", res.StatusCode) + } +}