Add testcase for Go private repositories (#2748)

Signed-off-by: Matej Vašek <mvasek@redhat.com>
This commit is contained in:
Matej Vašek 2025-03-17 04:49:36 +01:00 committed by GitHub
parent 90624a8725
commit 24a7fedadd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 462 additions and 0 deletions

View File

@ -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
}

View File

@ -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.

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,4 @@
specVersion: 0.36.0
name: go-fn
runtime: go
created: 2025-03-17T02:02:34.196208671+01:00

View File

@ -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

View File

@ -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=

View File

@ -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())
}

View File

@ -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)
}
}