fleet/pkg/git/fetch_test.go

498 lines
13 KiB
Go

package git_test
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"net/http/httptest"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/rancher/fleet/internal/config"
fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
"github.com/rancher/fleet/pkg/git"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func newTestClient(objs ...client.Object) client.Client {
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
return fake.NewClientBuilder().
WithObjects(objs...).
WithScheme(scheme).
Build()
}
func newTestGithubServer(refs []string, TLSCfg *tls.Config) *httptest.Server {
// fake response from github with capabilities
header := "001e# service=git-upload-pack\n01552ada7cca738877df8459b3a34839a15e5683edaa HEAD\x00"
header += "multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/master filter object-format=sha1 agent=git/github-f133c3a1d7e6\n"
response := header
for _, ref := range refs {
response += ref + "\n"
}
response += "0000\n"
mux := http.NewServeMux()
mux.HandleFunc("GET /v2/{$}", func(http.ResponseWriter, *http.Request) {
})
mux.HandleFunc("GET /info/refs", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, response)
})
ts := httptest.NewUnstartedServer(mux)
if TLSCfg != nil {
ts.TLS = TLSCfg
ts.StartTLS()
} else {
ts.Start()
}
return ts
}
var _ = Describe("git fetch's LatestCommit tests", func() {
var (
fakeGithub *httptest.Server
refs []string
)
JustBeforeEach(func() {
fakeGithub = newTestGithubServer(refs, nil)
})
AfterEach(func() {
fakeGithub.Close()
})
BeforeEach(func() {
refs = []string{
"003f2ada7cca738877df8459b3a34839a15e5683edaa refs/heads/master",
"004522a46b7cfd14db4c93c5fa1e27df1d6d7b6ef1da refs/heads/release/v0.5",
"0044f1be9e1bd0387fb6ec0df35f38b147a7016937e6 refs/heads/test-simple",
"003f56bca25f648a951c2f8fd6db4981e4a4f040ca4e refs/tags/example",
}
})
It("returns the commit for the expected revision", func() {
config.Set(&config.Config{
GitClientTimeout: metav1.Duration{Duration: 0},
})
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Type: corev1.SecretTypeBasicAuth,
Data: map[string][]byte{
corev1.BasicAuthUsernameKey: []byte("username"),
corev1.BasicAuthPasswordKey: []byte("password"),
},
}
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret-different",
Revision: "example",
Repo: fakeGithub.URL,
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient(secret)
f := git.Fetch{}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("56bca25f648a951c2f8fd6db4981e4a4f040ca4e"))
})
It("returns the commit for the expected branch", func() {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Type: corev1.SecretTypeBasicAuth,
Data: map[string][]byte{
corev1.BasicAuthUsernameKey: []byte("username"),
corev1.BasicAuthPasswordKey: []byte("password"),
},
}
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret",
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient(secret)
f := git.Fetch{}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
})
It("returns the commit for the expected branch with no secret", func() {
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret",
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient()
f := git.Fetch{
KnownHosts: mockKnownHostsGetter{
data: "foo",
},
}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
})
It("returns an error when secret's type is not expected", func() {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "test-ns",
},
Type: corev1.SecretTypeSSHAuth,
Data: map[string][]byte{
corev1.SSHAuthPrivateKey: []byte("Not_valid_key"),
"known_hosts": []byte("Not_valid_known_hosts"),
},
}
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret",
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient(secret)
f := git.Fetch{}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).To(HaveOccurred())
Expect(commit).To(BeEmpty())
Expect(err.Error()).To(Equal("ssh: no key found"))
})
It("returns an error when strict host key checks are enabled and known hosts checks fail for an SSH gitrepo", func() {
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret",
Repo: "ssh://foo.com/bar.git",
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient()
f := git.Fetch{
KnownHosts: mockKnownHostsGetter{
isStrict: true,
err: errors.New("something happened"),
},
}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).To(HaveOccurred())
Expect(commit).To(BeEmpty())
Expect(err.Error()).To(Equal("something happened"))
})
It("uses a Rancher CA bundle if configured", func() {
cfg, ca, err := setupCerts()
Expect(err).ToNot(HaveOccurred())
buf := make([]byte, base64.StdEncoding.EncodedLen(len(ca)))
base64.StdEncoding.Encode(buf, ca)
fakeGithub = newTestGithubServer(refs, cfg)
defer fakeGithub.Close()
config.Set(&config.Config{
GitClientTimeout: metav1.Duration{Duration: 0},
})
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "tls-ca-additional",
Namespace: "cattle-system",
},
Data: map[string][]byte{
"ca-additional.pem": ca,
},
}
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient(secret)
f := git.Fetch{}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
// Try again without the secret, and check that fetching the latest commit fails
c = newTestClient()
commit, err = f.LatestCommit(context.Background(), gr, c)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("signed by unknown authority"))
Expect(commit).To(BeEmpty())
})
It("succeeds despite known hosts errors for a non-SSH gitrepo", func() {
fakeGithub = newTestGithubServer(refs, nil)
defer fakeGithub.Close()
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
ClientSecretName: "test-secret",
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient()
f := git.Fetch{
KnownHosts: mockKnownHostsGetter{
isStrict: true,
err: errors.New("something happened"),
},
}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
})
It("uses a Rancher CA bundle if configured", func() {
cfg, ca, err := setupCerts()
Expect(err).ToNot(HaveOccurred())
buf := make([]byte, base64.StdEncoding.EncodedLen(len(ca)))
base64.StdEncoding.Encode(buf, ca)
fakeGithub = newTestGithubServer(refs, cfg)
defer fakeGithub.Close()
config.Set(&config.Config{
GitClientTimeout: metav1.Duration{Duration: 0},
})
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "tls-ca-additional",
Namespace: "cattle-system",
},
Data: map[string][]byte{
"ca-additional.pem": ca,
},
}
gr := &fleetv1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test-gitrepo",
Namespace: "test-ns",
},
Spec: fleetv1.GitRepoSpec{
Repo: fakeGithub.URL,
Branch: "master",
},
Status: fleetv1.GitRepoStatus{
Commit: "",
},
}
c := newTestClient(secret)
f := git.Fetch{}
commit, err := f.LatestCommit(context.Background(), gr, c)
Expect(err).ToNot(HaveOccurred())
Expect(commit).To(Equal("2ada7cca738877df8459b3a34839a15e5683edaa"))
// Try again without the secret, and check that fetching the latest commit fails
c = newTestClient()
commit, err = f.LatestCommit(context.Background(), gr, c)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Or(ContainSubstring("signed by unknown authority"), ContainSubstring("certificate is not standards compliant")))
Expect(commit).To(BeEmpty())
})
})
// setupCerts creates a CA certificate, encodes it in PEM format, and creates server certificates signed with the
// previously generated CA cert.
// It returns server TLS config used to set up a test server, along with PEM data for the CA cert and an error, if any
// (in which case the other 2 returned values will be nil).
// Heavily inspired by https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
func setupCerts() (serverTLSConf *tls.Config, caPEMData []byte, err error) {
subject := pkix.Name{
Organization: []string{"Testing Fleet, Inc."},
Country: []string{"DE"},
Locality: []string{"Fleet City"},
StreetAddress: []string{"Continuous Deployment Street"},
PostalCode: []string{"4242"},
}
// set up CA certificate
ca := &x509.Certificate{
SerialNumber: big.NewInt(2025),
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
caPEM := new(bytes.Buffer)
_ = pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
// set up server certificate
cert := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: subject,
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
if err != nil {
return nil, nil, err
}
certPEM := new(bytes.Buffer)
_ = pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
certPrivKeyPEM := new(bytes.Buffer)
_ = pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
if err != nil {
return nil, nil, err
}
serverTLSConf = &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
return serverTLSConf, caPEM.Bytes(), nil
}
type mockKnownHostsGetter struct {
isStrict bool
data string
err error
}
func (m mockKnownHostsGetter) IsStrict() bool {
return m.isStrict
}
func (m mockKnownHostsGetter) GetWithSecret(
ctx context.Context,
c client.Client,
secret *corev1.Secret,
) (string, error) {
return m.data, m.err
}