source-controller/internal/controller/gitrepository_controller_te...

3500 lines
112 KiB
Go

/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/go-git/go-billy/v5/memfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/memory"
. "github.com/onsi/gomega"
sshtestdata "golang.org/x/crypto/ssh/testdata"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/pkg/gittestserver"
"github.com/fluxcd/pkg/runtime/conditions"
conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check"
"github.com/fluxcd/pkg/runtime/jitter"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/ssh"
"github.com/fluxcd/pkg/testserver"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/features"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
)
const (
encodedCommitFixture = `tree 35f0b28987e60d4b8dec1f707fd07fef5ad84abc
parent 8b52742dbc848eb0975e62ae00fbfa4f8108e835
author Sanskar Jaiswal <jaiswalsanskar078@gmail.com> 1691045123 +0530
committer Sanskar Jaiswal <jaiswalsanskar078@gmail.com> 1691068951 +0530
git/e2e: disable CGO while running e2e tests
Disable CGO for Git e2e tests as it was originially required because of
our libgit2 client. Since we no longer maintain a libgit2 client, there
is no need to run the tests with CGO enabled.
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
`
malformedEncodedCommitFixture = `parent eb167bc68d0a11530923b1f24b4978535d10b879
author Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
committer Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
Update containerd and runc to fix CVEs
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
`
signatureCommitFixture = `-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEOxEY0f3iSZ5rKQ+vWYLQJ5wif/0FAmTLqnEACgkQWYLQJ5wi
f/1mYw/+LRttvfPrfYl7ASUBGYSQuDzjeold8OO1LpmwjrKPpX4ivZbXHh+lJF0F
fqudKuJfJzeQCHsMZjnfgvXHd2VvxPh1jX6h3JLuNu7d4g1DtNQsKJtsLx7JW99X
J9Bb1xj0Ghh2PkrWEB9vpw+uZz4IhFrB+DNNLRNBkon3etrS1q57q8dhQFIhLI1y
ij3rq3kFHjrNNdokIv2ujyVJtWgy2fK2ELW5v2dznpykOo7hQEKgtOIHPBzGBFT0
dUFjB99Qy4Qgjh3vWaY4fZ3u/vhp3swmw91OlDkFeyndWjDSZhzYnb7wY+U6z35C
aU4Gzc71CquSd/nTdOEkpuolBVWV5cBkM+Nxi8jtVGBeDDFE49j27a3lQ3+qtT7/
q4FCe5Jw3GSOJvaLBLGmYVn9fc49t/28b5tkGtCHs3ATpsJohzELEIiDP90Me7hQ
Joks3ML38T4J/zZ4/ObbVMkrCEATYe3r1Ep7+e6VmOG9iTg0JIexexddjHX26Tgu
iuVP2GD/8PceqgNW/LPX84Ub32WTKPZJg+NyliDjH5QOvmguK1dRtSb/9eyYcoSF
Fkf0HcgG5jOk0OZJv0QcqXd9PhB4oXeuXgGszo9M+fhr3nWvEooAJtIyLtVtt/u2
rNNB7xkZ1uWx+52w9RG2gmZh+LaESwd1rNXgUFLNBebNN3jNzsA=
=73xf
-----END PGP SIGNATURE-----`
encodedTagFixture = `object 11525516bd55152ce68848bb14680aad43f18479
type commit
tag v0.1.0
tagger Sanskar Jaiswal <jaiswalsanskar078@gmail.com> 1691132850 +0530
v0.1.0
`
malformedEncodedTagFixture = `object 11525516bd55152ce68848bb14680aad43f18479
tagger Sanskar Jaiswal <jaiswalsanskar078@gmail.com> 1691132850 +0530
v0.1.0
`
signatureTagFixture = `-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEOxEY0f3iSZ5rKQ+vWYLQJ5wif/0FAmTMo7IACgkQWYLQJ5wi
f/1uUQ/9F70u8LZZQ3+U2vuYQ8fyVp/AV5h5zwxK5UlkR1crB0gSpdaiIxMMQRc8
4QQIqCXloSHherUu9SPbDe9Qmr0JL8a57XqThjUSa52IYMDVos9sYwViJit+xGyz
HDot2nQ8MAqkDaiuwAnTqOyTPA89U36lGV/X/25mYxAuED+8xFx1OfvjGkX2eMEr
peWJ8VEfdFr2OmWwFceh6iF/izIaZGttwCyNy4BIh2W0GvUtQAxzqF4IzUvwfJU/
bgARaHKQhWqFhDNImttsqJBweWavEDDmUgNg80c3cUZKqBtAjElToP9gis/SnPH5
zaCAH66OzyKIhn6lde7KpOzyqbOyzddTa8SKkAAHyO7onukOktV8W9toeAxlF20q
Bw0MZGzAGisF8EK1HVv8UzrW9vAwdJN/yDIHWkjaeHr2FHmeV3a2QxH9PdwbE3tI
B21TCVULJuM8oR0ZG62xzg5ba5HiZMiilNMJdrBfjk5xYGk3LQU1gB4FVYa7yTsN
YfAokYtUIG187Qb8vPr1P95TzZxKdb7r/PAKEbGPro5D2Rri8OnxO/OaXG/giWS5
5gRGmsQjvMsbzE/2PVc9+jshtZM49xL9H3DMjAWtO6MFbOqGqdi4MBa0T4qj6sZz
AbSLuRIBpXDES86faDXLRmufc95+iA/fh7W23G6vmd+SjXnCcHc=
=o4nf
-----END PGP SIGNATURE-----
`
armoredKeyRingFixture = `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGQmiZ0BEACwsubUFoWtp6iJDK9oUN4RhPS0bAKpcRTa7P/rTCD/MbTMYdWC
4vod3FMm4+rNF0SESxY67MGmR4M3dSyOZkCijqHm9jDVOvN847LOl5bntkm8Euxm
LkpfsBWng09+gtfwuKxOxPMY017D1jM23OGbrqznHaokerFeDp9sJf1C7Z9jVf39
oB/MF0bMdUJuxFFBdpoI73DORlAVUI14mfDbFj7v02Spkv1hqS2LtJ/Jl4QR/Vw4
mR71aFmGFWqLBlkUOjJ2SZGkCmF/qbUdLmVb7yZUtqtua4DVkBPTORfOMhGDbrME
Nmb6Ft5neZwU0ETsT/oc6Np+PDFSUDBxu0CbKG6bw7N2y8RfiVJTaoNLFoFGV5dA
K8OpyTxU4IEPDMpkWs7tpRxPCC02uCfyqlvdF4EURXYXTj54DDLOGQjoqB+iGtVi
y2dQ4cuNhfuIFCFTA16s41DwmB0fQuOg3yfPPo7+jUefD+iAt3CZ9Guvu5+/mGyq
KxSBBRFHc8ED/L7JLPMU6tZglaPch9P4H6Fi2swDryyZQn/a2kYanEh9v1wL94L4
3gUdjIYP8kjfg7nnS2FX9hl5FtPeM3jvnWjfv9jR+c8HWQZY2wM3Rj5iulu70K2U
pkdRUN0p2D5+Kq6idNreNoPlpQGoUOYrtAfOwtDFgMwuOZ78XkSIbFhtgwARAQAB
tEVTYW5za2FyIEphaXN3YWwgKEdpdEh1YiBHUEcgc2lnaW5nIGtleSkgPGphaXN3
YWxzYW5za2FyMDc4QGdtYWlsLmNvbT6JAk4EEwEIADgWIQQ7ERjR/eJJnmspD69Z
gtAnnCJ//QUCZCaJnQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBZgtAn
nCJ//dF4D/0Tl5Wre6KrZvjDs5loulhN8YMYb63jr+x1eVkpMpta51XvZvkZFoiY
9T4MQX+qgAkTrUJsxgWUwtVtDfmbyLXodDRS6JUbCRiMu12VD7mNT+lUfuhR2sJv
rHZoolQp7X4DTea1R64PcttfmlGO2pUNpGNmhojO0PahXqOCHmEUWBJQhI8RvOcs
zRjEzDcAcEgtMGzamq6DR54YxyzGE8V9b5WD/elmEXM6uWW+CkfX8WskKbLdRY0t
+GQ1pOtf3tKxD46I3LIsUEwbyh4Dv4vJbZmyxjI+FKbSCW5tMrz/ZWrPNl0m+pDI
Yn0+GWed2pgTMFh3VAhYCyIVugKynlaToH+D2z3DnuEp3Jfs+b1BdirS/PW79tW7
rjCJzqofF2UPyK0mzdYL+P3k9Hip5J0bCGoeMdCLsP5fYq3Y1YS4bH4JkDm52y+r
y89AH4LHHQt+A7w19I+6M2jmcNnDUMrpuSo84GeoM59O3fU7hLCC1Jx4hj7EBRrb
QzY5FInrE/WTcgFRljK46zhW4ybmfak/xJV654UqJCDWlVbc68D8JrKNQOj7gdPs
zh1+m2pFDEhWZkaFtQbSEpXMIJ9DsCoyQL4Knl+89VxHsrIyAJsmGb3V8xvtv5w9
QuWtsDnYbvDHtTpu1NZChVrnr/l1k3C2fcLhV1s583AvhGMkbgSXkQ==
=Tdjz
-----END PGP PUBLIC KEY BLOCK-----
`
)
func TestGitRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
g := NewWithT(t)
namespaceName := "gitrepo-" + randStringRunes(5)
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
}
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
t.Cleanup(func() {
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
})
gitRepo := &sourcev1.GitRepository{}
gitRepo.Name = "test-gitrepo"
gitRepo.Namespace = namespaceName
gitRepo.Spec = sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
URL: "https://example.com",
}
// Add a test finalizer to prevent the object from getting deleted.
gitRepo.SetFinalizers([]string{"test-finalizer"})
g.Expect(k8sClient.Create(ctx, gitRepo)).NotTo(HaveOccurred())
// Add deletion timestamp by deleting the object.
g.Expect(k8sClient.Delete(ctx, gitRepo)).NotTo(HaveOccurred())
r := &GitRepositoryReconciler{
Client: k8sClient,
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
}
// NOTE: Only a real API server responds with an error in this scenario.
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(gitRepo)})
g.Expect(err).NotTo(HaveOccurred())
}
func TestGitRepositoryReconciler_Reconcile(t *testing.T) {
g := NewWithT(t)
server, err := gittestserver.NewTempGitServer()
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(server.Root())
server.AutoCreate()
g.Expect(server.StartHTTP()).To(Succeed())
defer server.StopHTTP()
repoPath := "/test.git"
_, err = initGitRepo(server, "testdata/git/repository", git.DefaultBranch, repoPath)
g.Expect(err).NotTo(HaveOccurred())
origObj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "gitrepository-reconcile-",
Namespace: "default",
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
URL: server.HTTPAddress() + repoPath,
},
}
obj := origObj.DeepCopy()
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
// Wait for finalizer to be set
g.Eventually(func() bool {
if err := testEnv.Get(ctx, key, obj); err != nil {
return false
}
return len(obj.Finalizers) > 0
}, timeout).Should(BeTrue())
// Wait for GitRepository to be Ready
waitForSourceReadyWithArtifact(ctx, g, obj)
// Check if the object status is valid.
condns := &conditionscheck.Conditions{NegativePolarity: gitRepositoryReadyCondition.NegativePolarity}
checker := conditionscheck.NewChecker(testEnv.Client, condns)
checker.WithT(g).CheckErr(ctx, obj)
// kstatus client conformance check.
u, err := patch.ToUnstructured(obj)
g.Expect(err).ToNot(HaveOccurred())
res, err := kstatus.Compute(u)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(res.Status).To(Equal(kstatus.CurrentStatus))
// Patch the object with reconcile request annotation.
patchHelper, err := patch.NewHelper(obj, testEnv.Client)
g.Expect(err).ToNot(HaveOccurred())
annotations := map[string]string{
meta.ReconcileRequestAnnotation: "now",
}
obj.SetAnnotations(annotations)
g.Expect(patchHelper.Patch(ctx, obj)).ToNot(HaveOccurred())
g.Eventually(func() bool {
if err := testEnv.Get(ctx, key, obj); err != nil {
return false
}
return obj.Status.LastHandledReconcileAt == "now"
}, timeout).Should(BeTrue())
g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
// Wait for GitRepository to be deleted
waitForSourceDeletion(ctx, g, obj)
// Check if a suspended object gets deleted.
obj = origObj.DeepCopy()
testSuspendedObjectDeleteWithArtifact(ctx, g, obj)
}
func TestGitRepositoryReconciler_reconcileSource_emptyRepository(t *testing.T) {
g := NewWithT(t)
server, err := gittestserver.NewTempGitServer()
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(server.Root())
server.AutoCreate()
g.Expect(server.StartHTTP()).To(Succeed())
defer server.StopHTTP()
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "empty-",
Generation: 1,
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
Timeout: &metav1.Duration{Duration: timeout},
URL: server.HTTPAddress() + "/test.git",
},
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
defer func() {
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
}()
var commit git.Commit
var includes artifactSet
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileSource(context.TODO(), sp, obj, &commit, &includes, t.TempDir())
assertConditions := []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "EmptyGitRepository", "git repository is empty"),
}
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(assertConditions))
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(Equal(sreconcile.ResultEmpty))
g.Expect(commit).ToNot(BeNil())
}
func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
type options struct {
username string
password string
publicKey []byte
privateKey []byte
ca []byte
}
tests := []struct {
name string
protocol string
server options
secret *corev1.Secret
beforeFunc func(obj *sourcev1.GitRepository)
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "HTTP without secretRef makes Reconciling=True",
protocol: "http",
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "HTTP with Basic Auth secret makes Reconciling=True",
protocol: "http",
server: options{
username: "git",
password: "1234",
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth",
},
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("1234"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "HTTPS with mutual TLS makes Reconciling=True",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mtls-certs",
},
Data: map[string][]byte{
"ca.crt": tlsCA,
"tls.crt": clientPublicKey,
"tls.key": clientPrivateKey,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "mtls-certs"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "HTTPS with mutual TLS and invalid private key makes CheckoutFailed=True and returns error",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-mtls-certs",
},
Data: map[string][]byte{
"ca.crt": tlsCA,
"tls.crt": clientPublicKey,
"tls.key": []byte("invalid"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-mtls-certs"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "foo")
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "tls: failed to find any PEM data in key input"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "foo"),
},
},
{
name: "HTTPS with CAFile secret makes Reconciling=True",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
},
Data: map[string][]byte{
"caFile": tlsCA,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "HTTPS with CAFile secret with both ca.crt and caFile keys makes Reconciling=True and ignores caFile",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ca-file",
},
Data: map[string][]byte{
"ca.crt": tlsCA,
"caFile": []byte("invalid"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "HTTPS with invalid CAFile secret makes CheckoutFailed=True and returns error",
protocol: "https",
server: options{
publicKey: tlsPublicKey,
privateKey: tlsPrivateKey,
ca: tlsCA,
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-ca",
},
Data: map[string][]byte{
"caFile": []byte("invalid"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "foo")
},
wantErr: true,
assertConditions: []metav1.Condition{
// The expected error messages may differ when in darwin. In some cases it will match the
// error message expected in linux: "x509: certificate signed by unknown authority". In
// other cases it may get "x509: “example.com” certificate is not standards compliant" instead.
//
// Trimming the expected error message for consistent results.
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "x509: "),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "foo"),
},
},
// TODO: Add test case for HTTPS with bearer token auth secret. It
// depends on gitkit to have support for bearer token based
// authentication.
{
name: "SSH with private key secret makes Reconciling=True",
protocol: "ssh",
server: options{
username: "git",
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "private-key",
},
Data: map[string][]byte{
"username": []byte("git"),
"identity": sshtestdata.PEMBytes["rsa"],
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "private-key"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>"),
},
},
{
name: "SSH with password protected private key secret makes Reconciling=True",
protocol: "ssh",
server: options{
username: "git",
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "private-key",
},
Data: map[string][]byte{
"username": []byte("git"),
"identity": sshtestdata.PEMEncryptedKeys[2].PEMBytes,
"password": []byte("password"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "private-key"}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
name: "Include get failure makes CheckoutFailed=True and returns error",
protocol: "http",
server: options{
username: "git",
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "foo")
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret '/non-existing': secrets \"non-existing\" not found"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "foo"),
},
},
{
name: "Existing artifact makes ArtifactOutdated=True",
protocol: "http",
server: options{
username: "git",
password: "1234",
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "basic-auth",
},
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("1234"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
obj.Status = sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "staging/some-revision",
Path: randStringRunes(10),
},
}
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master@sha1:<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:<commit>'"),
},
},
{
// This test is only for verifying the failure state when using
// provider auth. Protocol http is used for simplicity.
name: "github provider without secret ref makes FetchFailed=True",
protocol: "http",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "foo")
},
want: sreconcile.ResultEmpty,
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.InvalidProviderConfigurationReason, "secretRef with github app data must be specified when provider is set to github"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "foo"),
},
},
{
// This test is only for verifying the failure state when using
// provider auth. Protocol http is used for simplicity.
name: "empty provider with github app data in secret makes FetchFailed=True",
protocol: "http",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "github-app-secret",
},
Data: map[string][]byte{
github.AppIDKey: []byte("1111"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "github-app-secret"}
conditions.MarkReconciling(obj, meta.ProgressingReason, "foo")
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "foo")
},
want: sreconcile.ResultEmpty,
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.InvalidProviderConfigurationReason, "secretRef '/github-app-secret' has github app data but provider is not set to github"),
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "foo"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "auth-strategy-",
Generation: 1,
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
Timeout: &metav1.Duration{Duration: timeout},
},
}
server, err := gittestserver.NewTempGitServer()
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(server.Root())
server.AutoCreate()
repoPath := "/test.git"
localRepo, err := initGitRepo(server, "testdata/git/repository", git.DefaultBranch, repoPath)
g.Expect(err).NotTo(HaveOccurred())
if len(tt.server.username+tt.server.password) > 0 {
server.Auth(tt.server.username, tt.server.password)
}
secret := tt.secret.DeepCopy()
switch tt.protocol {
case "http":
g.Expect(server.StartHTTP()).To(Succeed())
defer server.StopHTTP()
obj.Spec.URL = server.HTTPAddress() + repoPath
case "https":
g.Expect(server.StartHTTPS(tt.server.publicKey, tt.server.privateKey, tt.server.ca, "example.com")).To(Succeed())
obj.Spec.URL = server.HTTPAddress() + repoPath
case "ssh":
server.KeyDir(filepath.Join(server.Root(), "keys"))
g.Expect(server.ListenSSH()).To(Succeed())
obj.Spec.URL = server.SSHAddress() + repoPath
go func() {
server.StartSSH()
}()
defer server.StopSSH()
if secret != nil && len(secret.Data["known_hosts"]) == 0 {
u, err := url.Parse(obj.Spec.URL)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(u.Host).ToNot(BeEmpty())
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
g.Expect(err).NotTo(HaveOccurred())
secret.Data["known_hosts"] = knownHosts
}
default:
t.Fatalf("unsupported protocol %q", tt.protocol)
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
if secret != nil {
clientBuilder.WithObjects(secret.DeepCopy())
}
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
tmpDir := t.TempDir()
head, _ := localRepo.Head()
assertConditions := tt.assertConditions
for k := range assertConditions {
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<commit>", head.Hash().String())
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", obj.Spec.URL)
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
defer func() {
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
}()
var commit git.Commit
var includes artifactSet
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileSource(context.TODO(), sp, obj, &commit, &includes, tmpDir)
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
g.Expect(commit).ToNot(BeNil())
// In-progress status condition validity.
checker := conditionscheck.NewInProgressChecker(r.Client)
checker.WithT(g).CheckErr(ctx, obj)
})
}
}
func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) {
tests := []struct {
name string
url string
secret *corev1.Secret
beforeFunc func(obj *sourcev1.GitRepository)
wantProviderOptsName string
wantErr error
}{
{
name: "azure provider",
url: "https://dev.azure.com/foo/bar/_git/baz",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderAzure
},
wantProviderOptsName: sourcev1.GitProviderAzure,
},
{
name: "github provider with no secret ref",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
},
wantProviderOptsName: sourcev1.GitProviderGitHub,
wantErr: errors.New("secretRef with github app data must be specified when provider is set to github"),
},
{
name: "github provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.AppIDKey: []byte("123"),
github.AppInstallationIDKey: []byte("456"),
github.AppPrivateKey: []byte("abc"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantProviderOptsName: sourcev1.GitProviderGitHub,
},
{
name: "generic provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.AppIDKey: []byte("123"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: errors.New("secretRef '/githubAppSecret' has github app data but provider is not set to github"),
},
{
name: "generic provider",
url: "https://example.com/org/repo",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
},
},
{
name: "secret ref defined for non existing secret",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "authSecret",
}
},
wantErr: errors.New("failed to get secret '/authSecret': secrets \"authSecret\" not found"),
},
{
url: "https://example.com/org/repo",
name: "no provider",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
if tt.secret != nil {
clientBuilder.WithObjects(tt.secret)
}
obj := &sourcev1.GitRepository{}
r := &GitRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32),
Client: clientBuilder.Build(),
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
url, err := url.Parse(tt.url)
g.Expect(err).ToNot(HaveOccurred())
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
opts, err := r.getAuthOpts(context.TODO(), obj, *url)
if tt.wantErr != nil {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(opts).ToNot(BeNil())
if tt.wantProviderOptsName != "" {
g.Expect(opts.ProviderOpts).ToNot(BeNil())
g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName))
} else {
g.Expect(opts.ProviderOpts).To(BeNil())
}
}
})
}
}
func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T) {
g := NewWithT(t)
branches := []string{"staging"}
tags := []string{"non-semver-tag", "v0.1.0", "0.2.0", "v0.2.1", "v1.0.0-alpha", "v1.1.0", "v2.0.0"}
refs := []string{"refs/pull/420/head"}
tests := []struct {
name string
reference *sourcev1.GitRepositoryRef
beforeFunc func(obj *sourcev1.GitRepository, latestRev string)
want sreconcile.Result
wantErr bool
wantRevision string
wantArtifactOutdated bool
wantReconciling bool
}{
{
name: "Nil reference (default branch)",
want: sreconcile.ResultSuccess,
wantRevision: "master@sha1:<commit>",
wantReconciling: true,
},
{
name: "Branch",
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
},
want: sreconcile.ResultSuccess,
wantRevision: "staging@sha1:<commit>",
wantReconciling: true,
},
{
name: "Tag",
reference: &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
},
want: sreconcile.ResultSuccess,
wantRevision: "v0.1.0@sha1:<commit>",
wantReconciling: true,
},
{
name: "Branch commit",
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
Commit: "<commit>",
},
want: sreconcile.ResultSuccess,
wantRevision: "staging@sha1:<commit>",
wantReconciling: true,
},
{
name: "Ref Name pointing to a branch",
reference: &sourcev1.GitRepositoryRef{
Name: "refs/heads/staging",
},
want: sreconcile.ResultSuccess,
wantRevision: "refs/heads/staging@sha1:<commit>",
wantReconciling: true,
},
{
name: "Ref Name pointing to a PR",
reference: &sourcev1.GitRepositoryRef{
Name: "refs/pull/420/head",
},
want: sreconcile.ResultSuccess,
wantRevision: "refs/pull/420/head@sha1:<commit>",
wantReconciling: true,
},
{
name: "SemVer",
reference: &sourcev1.GitRepositoryRef{
SemVer: "*",
},
want: sreconcile.ResultSuccess,
wantRevision: "v2.0.0@sha1:<commit>",
wantReconciling: true,
},
{
name: "SemVer range",
reference: &sourcev1.GitRepositoryRef{
SemVer: "<v0.2.1",
},
want: sreconcile.ResultSuccess,
wantRevision: "0.2.0@sha1:<commit>",
wantReconciling: true,
},
{
name: "SemVer prerelease",
reference: &sourcev1.GitRepositoryRef{
SemVer: ">=1.0.0-0 <1.1.0-0",
},
wantRevision: "v1.0.0-alpha@sha1:<commit>",
want: sreconcile.ResultSuccess,
wantReconciling: true,
},
{
name: "Existing artifact makes ArtifactOutdated=True",
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
},
beforeFunc: func(obj *sourcev1.GitRepository, latestRev string) {
obj.Status = sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "staging/some-revision",
Path: randStringRunes(10),
},
}
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "foo")
},
want: sreconcile.ResultSuccess,
wantRevision: "staging@sha1:<commit>",
wantArtifactOutdated: true,
wantReconciling: true,
},
{
name: "Optimized clone",
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
},
beforeFunc: func(obj *sourcev1.GitRepository, latestRev string) {
// Add existing artifact on the object and storage.
obj.Status = sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "staging@sha1:" + latestRev,
Path: randStringRunes(10),
},
}
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "foo")
},
want: sreconcile.ResultEmpty,
wantErr: true,
wantRevision: "staging@sha1:<commit>",
wantReconciling: false,
},
{
name: "Optimized clone different ignore",
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
},
beforeFunc: func(obj *sourcev1.GitRepository, latestRev string) {
// Set new ignore value.
obj.Spec.Ignore = ptr.To("foo")
// Add existing artifact on the object and storage.
obj.Status = sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "staging@sha1:" + latestRev,
Path: randStringRunes(10),
},
}
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "foo")
},
want: sreconcile.ResultSuccess,
wantRevision: "staging@sha1:<commit>",
wantReconciling: false,
},
}
server, err := gittestserver.NewTempGitServer()
g.Expect(err).To(BeNil())
defer os.RemoveAll(server.Root())
server.AutoCreate()
g.Expect(server.StartHTTP()).To(Succeed())
defer server.StopHTTP()
repoPath := "/test.git"
localRepo, err := initGitRepo(server, "testdata/git/repository", git.DefaultBranch, repoPath)
g.Expect(err).NotTo(HaveOccurred())
headRef, err := localRepo.Head()
g.Expect(err).NotTo(HaveOccurred())
for _, branch := range branches {
g.Expect(remoteBranchForHead(localRepo, headRef, branch)).To(Succeed())
}
for _, tag := range tags {
g.Expect(remoteTagForHead(localRepo, headRef, tag)).To(Succeed())
}
for _, ref := range refs {
g.Expect(remoteRefForHead(localRepo, headRef, ref)).To(Succeed())
}
r := &GitRepositoryReconciler{
Client: fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{}).
Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "checkout-strategy-",
Generation: 1,
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
Timeout: &metav1.Duration{Duration: timeout},
URL: server.HTTPAddress() + repoPath,
Reference: tt.reference,
},
}
if obj.Spec.Reference != nil && obj.Spec.Reference.Commit == "<commit>" {
obj.Spec.Reference.Commit = headRef.Hash().String()
}
tmpDir := t.TempDir()
if tt.beforeFunc != nil {
tt.beforeFunc(obj, headRef.Hash().String())
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
defer func() {
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
}()
var commit git.Commit
var includes artifactSet
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileSource(ctx, sp, obj, &commit, &includes, tmpDir)
if err != nil && !tt.wantErr {
t.Log(err)
}
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
if tt.wantRevision != "" && !tt.wantErr {
revision := strings.ReplaceAll(tt.wantRevision, "<commit>", headRef.Hash().String())
g.Expect(commitReference(obj, &commit)).To(Equal(revision))
g.Expect(conditions.IsTrue(obj, sourcev1.ArtifactOutdatedCondition)).To(Equal(tt.wantArtifactOutdated))
g.Expect(conditions.IsTrue(obj, meta.ReconcilingCondition)).To(Equal(tt.wantReconciling))
}
// In-progress status condition validity.
checker := conditionscheck.NewInProgressChecker(r.Client)
checker.WithT(g).CheckErr(ctx, obj)
})
}
}
func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
tests := []struct {
name string
dir string
includes artifactSet
beforeFunc func(obj *sourcev1.GitRepository)
afterFunc func(t *WithT, obj *sourcev1.GitRepository)
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "Archiving artifact to storage makes ArtifactInStorage=True",
dir: "testdata/git/repository",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Archiving artifact to storage with includes makes ArtifactInStorage=True",
dir: "testdata/git/repository",
includes: artifactSet{&sourcev1.Artifact{Revision: "main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91"}},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Include = []sourcev1.GitRepositoryInclude{
{GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}},
}
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Digest).To(Equal("sha256:34d9af1a2fcfaef3ee9487d67dc2d642bc7babdb9444a5f60d1f32df32e4de7d"))
t.Expect(obj.Status.IncludedArtifacts).ToNot(BeEmpty())
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Up-to-date artifact should not update status",
dir: "testdata/git/repository",
includes: artifactSet{&sourcev1.Artifact{Revision: "main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91", Digest: "some-checksum"}},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Include = []sourcev1.GitRepositoryInclude{
{GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}},
}
obj.Status.Artifact = &sourcev1.Artifact{Revision: "main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91"}
obj.Status.IncludedArtifacts = []*sourcev1.Artifact{{Revision: "main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91", Digest: "some-checksum"}}
obj.Status.ObservedInclude = obj.Spec.Include
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Spec ignore overwrite is taken into account",
dir: "testdata/git/repository",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Ignore = ptr.To("!**.txt\n")
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Digest).To(Equal("sha256:a17037f96f541a47bdadcd12ab40b943c50a9ffd25dc8a30a5e9af52971fd94f"))
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Source ignore for subdir ignore patterns",
dir: "testdata/git/repowithsubdirs",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Digest).To(Equal("sha256:ad9943d761b30e943e2a770ea9083a40fc03f09846efd61f6c442cc48fefad11"))
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Removes ArtifactOutdatedCondition after creating new artifact",
dir: "testdata/git/repository",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "")
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Digest).To(Equal("sha256:34d9af1a2fcfaef3ee9487d67dc2d642bc7babdb9444a5f60d1f32df32e4de7d"))
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision 'main@sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'"),
},
},
{
name: "Target path does not exists",
dir: "testdata/git/foo",
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.StorageOperationFailedCondition, sourcev1.StatOperationFailedReason, "failed to stat target artifact path"),
},
},
{
name: "Target path is not a directory",
dir: "testdata/git/repository/foo.txt",
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.StorageOperationFailedCondition, sourcev1.InvalidPathReason, "invalid target path"),
},
},
}
artifactSize := func(g *WithT, artifactURL string) *int64 {
if artifactURL == "" {
return nil
}
res, err := http.Get(artifactURL)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(res.StatusCode).To(Equal(http.StatusOK))
defer res.Body.Close()
return &res.ContentLength
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
resetChmod(tt.dir, 0o750, 0o600)
r := &GitRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "reconcile-artifact-",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{},
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
commit := git.Commit{
Hash: []byte("b9b3feadba509cb9b22e968a5d27e96c2bc2ff91"),
Reference: "refs/heads/main",
}
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileArtifact(ctx, sp, obj, &commit, &tt.includes, tt.dir)
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
if obj.Status.Artifact != nil {
g.Expect(obj.Status.Artifact.Size).To(Equal(artifactSize(g, obj.Status.Artifact.URL)))
}
if tt.afterFunc != nil {
tt.afterFunc(g, obj)
}
})
}
}
func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
g := NewWithT(t)
server, err := testserver.NewTempArtifactServer()
g.Expect(err).NotTo(HaveOccurred())
storage, err := newTestStorage(server.HTTPServer)
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(storage.BasePath)
dependencyInterval := 5 * time.Second
type dependency struct {
name string
withArtifact bool
conditions []metav1.Condition
}
type include struct {
name string
fromPath string
toPath string
shouldExist bool
}
tests := []struct {
name string
dependencies []dependency
includes []include
beforeFunc func(obj *sourcev1.GitRepository)
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "New includes make ArtifactOutdated=True",
dependencies: []dependency{
{
name: "a",
withArtifact: true,
conditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Foo", "foo ready"),
},
},
{
name: "b",
withArtifact: true,
conditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Bar", "bar ready"),
},
},
},
includes: []include{
{name: "a", toPath: "a/", shouldExist: true},
{name: "b", toPath: "b/", shouldExist: true},
},
want: sreconcile.ResultSuccess,
},
{
name: "Invalid FromPath makes IncludeUnavailable=True and returns error",
dependencies: []dependency{
{
name: "a",
withArtifact: true,
},
},
includes: []include{
{name: "a", fromPath: "../../../path", shouldExist: false},
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.StorageOperationFailedCondition, "CopyFailure", "unpack/path: no such file or directory"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
var depObjs []client.Object
for _, d := range tt.dependencies {
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: d.name,
},
Status: sourcev1.GitRepositoryStatus{
Conditions: d.conditions,
},
}
if d.withArtifact {
obj.Status.Artifact = &sourcev1.Artifact{
Path: d.name + ".tar.gz",
Revision: d.name,
LastUpdateTime: metav1.Now(),
}
g.Expect(storage.Archive(obj.GetArtifact(), "testdata/git/repository", nil)).To(Succeed())
}
depObjs = append(depObjs, obj)
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
if len(tt.dependencies) > 0 {
clientBuilder.WithObjects(depObjs...)
}
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: storage,
requeueDependency: dependencyInterval,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "reconcile-include",
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
},
}
for i, incl := range tt.includes {
incl := sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: incl.name},
FromPath: incl.fromPath,
ToPath: incl.toPath,
}
tt.includes[i].fromPath = incl.GetFromPath()
tt.includes[i].toPath = incl.GetToPath()
obj.Spec.Include = append(obj.Spec.Include, incl)
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
tmpDir := t.TempDir()
var commit git.Commit
var includes artifactSet
// Build includes artifactSet.
artifactSet, err := r.fetchIncludes(ctx, obj)
g.Expect(err).ToNot(HaveOccurred())
includes = *artifactSet
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileInclude(ctx, sp, obj, &commit, &includes, tmpDir)
g.Expect(obj.GetConditions()).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
if err == nil {
g.Expect(len(includes)).To(Equal(len(tt.includes)))
}
g.Expect(got).To(Equal(tt.want))
for _, i := range tt.includes {
if i.toPath != "" {
expect := g.Expect(filepath.Join(tmpDir, i.toPath))
if i.shouldExist {
expect.To(BeADirectory())
} else {
expect.NotTo(BeADirectory())
}
}
if i.shouldExist {
g.Expect(filepath.Join(tmpDir, i.toPath)).Should(BeADirectory())
} else {
g.Expect(filepath.Join(tmpDir, i.toPath)).ShouldNot(BeADirectory())
}
}
})
}
}
func TestGitRepositoryReconciler_reconcileStorage(t *testing.T) {
tests := []struct {
name string
beforeFunc func(obj *sourcev1.GitRepository, storage *Storage) error
want sreconcile.Result
wantErr bool
assertArtifact *sourcev1.Artifact
assertConditions []metav1.Condition
assertPaths []string
}{
{
name: "garbage collects",
beforeFunc: func(obj *sourcev1.GitRepository, storage *Storage) error {
revisions := []string{"a", "b", "c", "d"}
for n := range revisions {
v := revisions[n]
obj.Status.Artifact = &sourcev1.Artifact{
Path: fmt.Sprintf("/reconcile-storage/%s.txt", v),
Revision: v,
}
if err := storage.MkdirAll(*obj.Status.Artifact); err != nil {
return err
}
if err := storage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0o640); err != nil {
return err
}
if n != len(revisions)-1 {
time.Sleep(time.Second * 1)
}
}
storage.SetArtifactURL(obj.Status.Artifact)
conditions.MarkTrue(obj, meta.ReadyCondition, "foo", "bar")
return nil
},
assertArtifact: &sourcev1.Artifact{
Path: "/reconcile-storage/d.txt",
Revision: "d",
Digest: "sha256:18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4",
URL: testStorage.Hostname + "/reconcile-storage/d.txt",
Size: int64p(int64(len("d"))),
},
assertPaths: []string{
"/reconcile-storage/d.txt",
"/reconcile-storage/c.txt",
"!/reconcile-storage/b.txt",
"!/reconcile-storage/a.txt",
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "foo", "bar"),
},
},
{
name: "build artifact first time",
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact"),
},
},
{
name: "notices missing artifact in storage",
beforeFunc: func(obj *sourcev1.GitRepository, storage *Storage) error {
obj.Status.Artifact = &sourcev1.Artifact{
Path: "/reconcile-storage/invalid.txt",
Revision: "e",
}
storage.SetArtifactURL(obj.Status.Artifact)
return nil
},
want: sreconcile.ResultSuccess,
assertPaths: []string{
"!/reconcile-storage/invalid.txt",
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
},
},
{
name: "notices empty artifact digest",
beforeFunc: func(obj *sourcev1.GitRepository, storage *Storage) error {
f := "empty-digest.txt"
obj.Status.Artifact = &sourcev1.Artifact{
Path: fmt.Sprintf("/reconcile-storage/%s.txt", f),
Revision: "fake",
}
if err := storage.MkdirAll(*obj.Status.Artifact); err != nil {
return err
}
if err := storage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(f), 0o600); err != nil {
return err
}
// Overwrite with a different digest
obj.Status.Artifact.Digest = ""
return nil
},
want: sreconcile.ResultSuccess,
assertPaths: []string{
"!/reconcile-storage/empty-digest.txt",
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
},
},
{
name: "notices artifact digest mismatch",
beforeFunc: func(obj *sourcev1.GitRepository, storage *Storage) error {
f := "digest-mismatch.txt"
obj.Status.Artifact = &sourcev1.Artifact{
Path: fmt.Sprintf("/reconcile-storage/%s.txt", f),
Revision: "fake",
}
if err := storage.MkdirAll(*obj.Status.Artifact); err != nil {
return err
}
if err := storage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(f), 0o600); err != nil {
return err
}
// Overwrite with a different digest
obj.Status.Artifact.Digest = "sha256:6c329d5322473f904e2f908a51c12efa0ca8aa4201dd84f2c9d203a6ab3e9023"
return nil
},
want: sreconcile.ResultSuccess,
assertPaths: []string{
"!/reconcile-storage/digest-mismatch.txt",
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: disappeared from storage"),
},
},
{
name: "updates hostname on diff from current",
beforeFunc: func(obj *sourcev1.GitRepository, storage *Storage) error {
obj.Status.Artifact = &sourcev1.Artifact{
Path: "/reconcile-storage/hostname.txt",
Revision: "f",
Digest: "sha256:3b9c358f36f0a31b6ad3e14f309c7cf198ac9246e8316f9ce543d5b19ac02b80",
URL: "http://outdated.com/reconcile-storage/hostname.txt",
}
if err := storage.MkdirAll(*obj.Status.Artifact); err != nil {
return err
}
if err := storage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader("file"), 0o640); err != nil {
return err
}
conditions.MarkTrue(obj, meta.ReadyCondition, "foo", "bar")
return nil
},
want: sreconcile.ResultSuccess,
assertPaths: []string{
"/reconcile-storage/hostname.txt",
},
assertArtifact: &sourcev1.Artifact{
Path: "/reconcile-storage/hostname.txt",
Revision: "f",
Digest: "sha256:3b9c358f36f0a31b6ad3e14f309c7cf198ac9246e8316f9ce543d5b19ac02b80",
URL: testStorage.Hostname + "/reconcile-storage/hostname.txt",
Size: int64p(int64(len("file"))),
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "foo", "bar"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
defer func() {
g.Expect(os.RemoveAll(filepath.Join(testStorage.BasePath, "/reconcile-storage"))).To(Succeed())
}()
r := &GitRepositoryReconciler{
Client: fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{}).
Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
Generation: 1,
},
}
if tt.beforeFunc != nil {
g.Expect(tt.beforeFunc(obj, testStorage)).To(Succeed())
}
g.Expect(r.Client.Create(context.TODO(), obj)).ToNot(HaveOccurred())
defer func() {
g.Expect(r.Client.Delete(context.TODO(), obj)).ToNot(HaveOccurred())
}()
var c *git.Commit
var as artifactSet
sp := patch.NewSerialPatcher(obj, r.Client)
got, err := r.reconcileStorage(context.TODO(), sp, obj, c, &as, "")
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
g.Expect(obj.Status.Artifact).To(MatchArtifact(tt.assertArtifact))
if tt.assertArtifact != nil && tt.assertArtifact.URL != "" {
g.Expect(obj.Status.Artifact.URL).To(Equal(tt.assertArtifact.URL))
}
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
for _, p := range tt.assertPaths {
absoluteP := filepath.Join(testStorage.BasePath, p)
if !strings.HasPrefix(p, "!") {
g.Expect(absoluteP).To(BeAnExistingFile())
continue
}
g.Expect(absoluteP).NotTo(BeAnExistingFile())
}
// In-progress status condition validity.
checker := conditionscheck.NewInProgressChecker(r.Client)
checker.WithT(g).CheckErr(ctx, obj)
})
}
}
func TestGitRepositoryReconciler_reconcileDelete(t *testing.T) {
g := NewWithT(t)
r := &GitRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "reconcile-delete-",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
sourcev1.SourceFinalizer,
},
},
Status: sourcev1.GitRepositoryStatus{},
}
artifact := testStorage.NewArtifactFor(sourcev1.GitRepositoryKind, obj.GetObjectMeta(), "revision", "foo.txt")
obj.Status.Artifact = &artifact
got, err := r.reconcileDelete(ctx, obj)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(got).To(Equal(sreconcile.ResultEmpty))
g.Expect(controllerutil.ContainsFinalizer(obj, sourcev1.SourceFinalizer)).To(BeFalse())
g.Expect(obj.Status.Artifact).To(BeNil())
}
func TestGitRepositoryReconciler_verifySignature(t *testing.T) {
tests := []struct {
name string
secret *corev1.Secret
commit git.Commit
beforeFunc func(obj *sourcev1.GitRepository)
want sreconcile.Result
wantErr bool
err error
wantSourceVerificationMode *sourcev1.GitVerificationMode
assertConditions []metav1.Condition
}{
{
name: "Valid commit with mode=HEAD makes SourceVerifiedCondition=True",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(encodedCommitFixture),
Signature: signatureCommitFixture,
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
want: sreconcile.ResultSuccess,
wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- commit 'shasum' with key '5982D0279C227FFD'"),
},
},
{
name: "Valid commit with mode=head makes SourceVerifiedCondition=True",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(encodedCommitFixture),
Signature: signatureCommitFixture,
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: "head",
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
want: sreconcile.ResultSuccess,
wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- commit 'shasum' with key '5982D0279C227FFD'"),
},
},
{
name: "Valid tag with mode=tag makes SourceVerifiedCondition=True",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
ReferencingTag: &git.Tag{
Name: "v0.1.0",
Hash: []byte("shasum"),
Encoded: []byte(encodedTagFixture),
Signature: signatureTagFixture,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
want: sreconcile.ResultSuccess,
wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTag),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key '5982D0279C227FFD'"),
},
},
{
name: "Valid tag and commit with mode=TagAndHEAD makes SourceVerifiedCondition=True",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(encodedCommitFixture),
Signature: signatureCommitFixture,
ReferencingTag: &git.Tag{
Name: "v0.1.0",
Hash: []byte("shasum"),
Encoded: []byte(encodedTagFixture),
Signature: signatureTagFixture,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTagAndHEAD,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
want: sreconcile.ResultSuccess,
wantSourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD),
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of\n\t- tag 'v0.1.0@shasum' with key '5982D0279C227FFD'\n\t- commit 'shasum' with key '5982D0279C227FFD'"),
},
},
{
name: "Source verification mode in status is unset if there's no verification in spec",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.SourceVerificationMode = ptrToVerificationMode(sourcev1.ModeGitHEAD)
obj.Spec.Verification = nil
},
want: sreconcile.ResultSuccess,
},
{
name: "Verification of tag with no tag ref SourceVerifiedCondition=False and returns a stalling error",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Branch: "main",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
wantErr: true,
err: serror.NewStalling(
errors.New("cannot verify tag object's signature if a tag reference is not specified"),
"InvalidVerificationMode",
),
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidVerificationMode", "cannot verify tag object's signature if a tag reference is not specified"),
},
},
{
name: "Unsigned tag with mode=tag makes SourceVerifiedCondition=False",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
ReferencingTag: &git.Tag{
Name: "v0.1.0",
Hash: []byte("shasum"),
Encoded: []byte(encodedTagFixture),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidGitObject", "cannot verify signature of tag 'v0.1.0@shasum' since it is not signed"),
},
},
{
name: "Partially successful verification makes SourceVerifiedCondition=False",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(malformedEncodedCommitFixture),
Signature: signatureCommitFixture,
ReferencingTag: &git.Tag{
Name: "v0.1.0",
Hash: []byte("shasum"),
Encoded: []byte(encodedTagFixture),
Signature: signatureTagFixture,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTagAndHEAD,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify Git commit: unable to verify payload with any of the given key rings"),
},
},
{
name: "Invalid commit makes SourceVerifiedCondition=False and returns error",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(malformedEncodedCommitFixture),
Signature: signatureCommitFixture,
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify Git commit: unable to verify payload with any of the given key rings"),
},
},
{
name: "Invalid tag signature with mode=tag makes SourceVerifiedCondition=False",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "existing",
},
Data: map[string][]byte{
"foo": []byte(armoredKeyRingFixture),
},
},
commit: git.Commit{
ReferencingTag: &git.Tag{
Name: "v0.1.0",
Hash: []byte("shasum"),
Encoded: []byte(malformedEncodedTagFixture),
Signature: signatureTagFixture,
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
}
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
SecretRef: meta.LocalObjectReference{
Name: "existing",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidTagSignature", "signature verification of tag 'v0.1.0@shasum' failed: unable to verify Git tag: unable to verify payload with any of the given key rings"),
},
},
{
name: "Invalid PGP key makes SourceVerifiedCondition=False and returns error",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid",
},
Data: map[string][]byte{
"foo": []byte("invalid PGP public key"),
},
},
commit: git.Commit{
Hash: []byte("shasum"),
Encoded: []byte(malformedEncodedCommitFixture),
Signature: signatureCommitFixture,
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
SecretRef: meta.LocalObjectReference{
Name: "invalid",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify Git commit: unable to read armored key ring: openpgp: invalid argument: no armored data found"),
},
},
{
name: "Secret get failure makes SourceVerifiedCondition=False and returns error",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
SecretRef: meta.LocalObjectReference{
Name: "none-existing",
},
}
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "VerificationError", "PGP public keys secret error: secrets \"none-existing\" not found"),
},
},
{
name: "Nil verification in spec deletes SourceVerified condition",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{},
},
{
name: "Empty verification mode in spec deletes SourceVerified condition",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
if tt.secret != nil {
clientBuilder.WithObjects(tt.secret)
}
r := &GitRepositoryReconciler{
EventRecorder: record.NewFakeRecorder(32),
Client: clientBuilder.Build(),
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "verify-commit-",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{},
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
got, err := r.verifySignature(context.TODO(), obj, tt.commit)
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
if tt.err != nil {
g.Expect(err).To(Equal(tt.err))
}
g.Expect(got).To(Equal(tt.want))
if tt.wantSourceVerificationMode != nil {
g.Expect(*obj.Status.SourceVerificationMode).To(Equal(*tt.wantSourceVerificationMode))
} else {
g.Expect(obj.Status.SourceVerificationMode).To(BeNil())
}
})
}
}
func TestGitRepositoryReconciler_getProxyOpts(t *testing.T) {
invalidProxy := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-proxy",
Namespace: "default",
},
Data: map[string][]byte{
"url": []byte("https://example.com"),
},
}
validProxy := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-proxy",
Namespace: "default",
},
Data: map[string][]byte{
"address": []byte("https://example.com"),
"username": []byte("user"),
"password": []byte("pass"),
},
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithObjects(invalidProxy, validProxy)
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
}
tests := []struct {
name string
secret string
err string
proxyOpts *transport.ProxyOptions
}{
{
name: "non-existent secret",
secret: "non-existent",
err: "failed to get proxy secret 'default/non-existent': ",
},
{
name: "invalid proxy secret",
secret: "invalid-proxy",
err: "invalid proxy secret 'default/invalid-proxy': key 'address' is missing",
},
{
name: "valid proxy secret",
secret: "valid-proxy",
proxyOpts: &transport.ProxyOptions{
URL: "https://example.com",
Username: "user",
Password: "pass",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
opts, err := r.getProxyOpts(context.TODO(), tt.secret, "default")
if opts != nil {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(opts).To(Equal(tt.proxyOpts))
} else {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.err))
}
})
}
}
func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
g := NewWithT(t)
server, err := gittestserver.NewTempGitServer()
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(server.Root())
server.AutoCreate()
g.Expect(server.StartHTTP()).To(Succeed())
defer server.StopHTTP()
repoPath := "/test.git"
_, err = initGitRepo(server, "testdata/git/repository", git.DefaultBranch, repoPath)
g.Expect(err).NotTo(HaveOccurred())
tests := []struct {
name string
beforeFunc func(obj *sourcev1.GitRepository)
want ctrl.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "no failure condition",
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
{
name: "reconciling condition",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, meta.ReconcilingCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
{
name: "stalled condition",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, meta.StalledCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
{
name: "mixed failed conditions",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
{
name: "reconciling and failed conditions",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, meta.ReconcilingCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
{
name: "stalled and failed conditions",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, meta.StalledCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, "Succeeded", "stored artifact for revision"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "condition-update",
Namespace: "default",
Finalizers: []string{sourcev1.SourceFinalizer},
},
Spec: sourcev1.GitRepositorySpec{
URL: server.HTTPAddress() + repoPath,
Interval: metav1.Duration{Duration: interval},
Timeout: &metav1.Duration{Duration: timeout},
},
}
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithObjects(obj).
WithStatusSubresource(&sourcev1.GitRepository{})
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
key := client.ObjectKeyFromObject(obj)
res, err := r.Reconcile(context.TODO(), ctrl.Request{NamespacedName: key})
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(res).To(Equal(tt.want))
updatedObj := &sourcev1.GitRepository{}
err = r.Get(ctx, key, updatedObj)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(updatedObj.GetConditions()).To(conditions.MatchConditions(tt.assertConditions))
})
}
}
// helpers
func initGitRepo(server *gittestserver.GitServer, fixture, branch, repositoryPath string) (*gogit.Repository, error) {
fs := memfs.New()
repo, err := gogit.Init(memory.NewStorage(), fs)
if err != nil {
return nil, err
}
branchRef := plumbing.NewBranchReferenceName(branch)
if err = repo.CreateBranch(&config.Branch{
Name: branch,
Remote: gogit.DefaultRemoteName,
Merge: branchRef,
}); err != nil {
return nil, err
}
err = commitFromFixture(repo, fixture)
if err != nil {
return nil, err
}
if server.HTTPAddress() == "" {
if err = server.StartHTTP(); err != nil {
return nil, err
}
defer server.StopHTTP()
}
if _, err = repo.CreateRemote(&config.RemoteConfig{
Name: gogit.DefaultRemoteName,
URLs: []string{server.HTTPAddressWithCredentials() + repositoryPath},
}); err != nil {
return nil, err
}
if err = repo.Push(&gogit.PushOptions{
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
}); err != nil {
return nil, err
}
return repo, nil
}
func Test_commitFromFixture(t *testing.T) {
g := NewWithT(t)
repo, err := gogit.Init(memory.NewStorage(), memfs.New())
g.Expect(err).ToNot(HaveOccurred())
err = commitFromFixture(repo, "testdata/git/repository")
g.Expect(err).ToNot(HaveOccurred())
}
func commitFromFixture(repo *gogit.Repository, fixture string) error {
working, err := repo.Worktree()
if err != nil {
return err
}
fs := working.Filesystem
if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
}
fileBytes, err := os.ReadFile(path)
if err != nil {
return err
}
ff, err := fs.Create(path[len(fixture):])
if err != nil {
return err
}
defer ff.Close()
_, err = ff.Write(fileBytes)
return err
}); err != nil {
return err
}
_, err = working.Add(".")
if err != nil {
return err
}
if _, err = working.Commit("Fixtures from "+fixture, &gogit.CommitOptions{
Author: &object.Signature{
Name: "Jane Doe",
Email: "jane@example.com",
When: time.Now(),
},
}); err != nil {
return err
}
return nil
}
func remoteBranchForHead(repo *gogit.Repository, head *plumbing.Reference, branch string) error {
refSpec := fmt.Sprintf("%s:refs/heads/%s", head.Name(), branch)
return repo.Push(&gogit.PushOptions{
RemoteName: "origin",
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
Force: true,
})
}
func remoteTagForHead(repo *gogit.Repository, head *plumbing.Reference, tag string) error {
if _, err := repo.CreateTag(tag, head.Hash(), &gogit.CreateTagOptions{
// Not setting this seems to make things flaky
// Expected success, but got an error:
// <*errors.errorString | 0xc0000f6350>: {
// s: "tagger field is required",
// }
// tagger field is required
Tagger: &object.Signature{
Name: "Jane Doe",
Email: "jane@example.com",
When: time.Now(),
},
Message: tag,
}); err != nil {
return err
}
refSpec := fmt.Sprintf("refs/tags/%[1]s:refs/tags/%[1]s", tag)
return repo.Push(&gogit.PushOptions{
RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
})
}
func remoteRefForHead(repo *gogit.Repository, head *plumbing.Reference, reference string) error {
if err := repo.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(reference), head.Hash())); err != nil {
return err
}
if err := repo.Push(&gogit.PushOptions{
RefSpecs: []config.RefSpec{
config.RefSpec("+" + reference + ":" + reference),
},
}); err != nil {
return err
}
return nil
}
func TestGitRepositoryReconciler_statusConditions(t *testing.T) {
tests := []struct {
name string
beforeFunc func(obj *sourcev1.GitRepository)
assertConditions []metav1.Condition
wantErr bool
}{
{
name: "multiple positive conditions",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision")
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of commit")
},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of commit"),
},
},
{
name: "multiple failures",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret")
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "IllegalPath", "some error")
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, sourcev1.DirCreationFailedReason, "failed to create directory")
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "some error")
},
assertConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.DirCreationFailedReason, "failed to create directory"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret"),
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "IllegalPath", "some error"),
*conditions.TrueCondition(sourcev1.StorageOperationFailedCondition, sourcev1.DirCreationFailedReason, "failed to create directory"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "some error"),
},
wantErr: true,
},
{
name: "mixed positive and negative conditions",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision")
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret")
},
assertConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get secret"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret"),
*conditions.TrueCondition(sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "stored artifact for revision"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.GitRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: "gitrepo",
Namespace: "foo",
},
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.Scheme()).
WithObjects(obj).
WithStatusSubresource(&sourcev1.GitRepository{})
c := clientBuilder.Build()
serialPatcher := patch.NewSerialPatcher(obj, c)
if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
ctx := context.TODO()
summarizeHelper := summarize.NewHelper(record.NewFakeRecorder(32), serialPatcher)
summarizeOpts := []summarize.Option{
summarize.WithConditions(gitRepositoryReadyCondition),
summarize.WithBiPolarityConditionTypes(sourcev1.SourceVerifiedCondition),
summarize.WithReconcileResult(sreconcile.ResultSuccess),
summarize.WithIgnoreNotFound(),
summarize.WithResultBuilder(sreconcile.AlwaysRequeueResultBuilder{
RequeueAfter: jitter.JitteredIntervalDuration(obj.GetRequeueAfter()),
}),
summarize.WithPatchFieldOwner("source-controller"),
}
_, err := summarizeHelper.SummarizeAndPatch(ctx, obj, summarizeOpts...)
g.Expect(err != nil).To(Equal(tt.wantErr))
key := client.ObjectKeyFromObject(obj)
g.Expect(c.Get(ctx, key, obj)).ToNot(HaveOccurred())
g.Expect(obj.GetConditions()).To(conditions.MatchConditions(tt.assertConditions))
})
}
}
func TestGitRepositoryReconciler_notify(t *testing.T) {
concreteCommit := git.Commit{
Hash: git.Hash("b9b3feadba509cb9b22e968a5d27e96c2bc2ff91"),
Message: "test commit",
Encoded: []byte("content"),
}
partialCommit := git.Commit{
Hash: git.Hash("b9b3feadba509cb9b22e968a5d27e96c2bc2ff91"),
}
noopErr := serror.NewGeneric(fmt.Errorf("some no-op error"), "NoOpReason")
noopErr.Ignore = true
tests := []struct {
name string
res sreconcile.Result
resErr error
oldObjBeforeFunc func(obj *sourcev1.GitRepository)
newObjBeforeFunc func(obj *sourcev1.GitRepository)
commit git.Commit
wantEvent string
}{
{
name: "error - no event",
res: sreconcile.ResultEmpty,
resErr: errors.New("some error"),
},
{
name: "new artifact",
res: sreconcile.ResultSuccess,
resErr: nil,
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
},
commit: concreteCommit,
wantEvent: "Normal NewArtifact stored artifact for commit 'test commit'",
},
{
name: "recovery from failure",
res: sreconcile.ResultSuccess,
resErr: nil,
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "foo")
},
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
},
commit: concreteCommit,
wantEvent: "Normal Succeeded stored artifact for commit 'test commit'",
},
{
name: "recovery and new artifact",
res: sreconcile.ResultSuccess,
resErr: nil,
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "foo")
},
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "aaa", Digest: "bbb"}
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
},
commit: concreteCommit,
wantEvent: "Normal NewArtifact stored artifact for commit 'test commit'",
},
{
name: "no updates",
res: sreconcile.ResultSuccess,
resErr: nil,
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
},
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
},
},
{
name: "no-op error result",
res: sreconcile.ResultEmpty,
resErr: noopErr,
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "foo")
},
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Digest: "yyy"}
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
},
commit: partialCommit, // no-op will always result in partial commit.
wantEvent: "Normal Succeeded stored artifact for commit 'sha1:b9b3feadba509cb9b22e968a5d27e96c2bc2ff91'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
recorder := record.NewFakeRecorder(32)
oldObj := &sourcev1.GitRepository{}
newObj := oldObj.DeepCopy()
if tt.oldObjBeforeFunc != nil {
tt.oldObjBeforeFunc(oldObj)
}
if tt.newObjBeforeFunc != nil {
tt.newObjBeforeFunc(newObj)
}
reconciler := &GitRepositoryReconciler{
EventRecorder: recorder,
features: features.FeatureGates(),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
reconciler.notify(ctx, oldObj, newObj, tt.commit, tt.res, tt.resErr)
select {
case x, ok := <-recorder.Events:
g.Expect(ok).To(Equal(tt.wantEvent != ""), "unexpected event received")
if tt.wantEvent != "" {
g.Expect(x).To(ContainSubstring(tt.wantEvent))
}
default:
if tt.wantEvent != "" {
t.Errorf("expected some event to be emitted")
}
}
})
}
}
func TestGitRepositoryReconciler_fetchIncludes(t *testing.T) {
type dependency struct {
name string
withArtifact bool
conditions []metav1.Condition
}
type include struct {
name string
fromPath string
toPath string
shouldExist bool
}
tests := []struct {
name string
dependencies []dependency
includes []include
beforeFunc func(obj *sourcev1.GitRepository)
wantErr bool
wantArtifactSet artifactSet
assertConditions []metav1.Condition
}{
{
name: "Existing includes",
dependencies: []dependency{
{
name: "a",
withArtifact: true,
conditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Foo", "foo ready"),
},
},
{
name: "b",
withArtifact: true,
conditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Bar", "bar ready"),
},
},
},
includes: []include{
{name: "a", toPath: "a/", shouldExist: true},
{name: "b", toPath: "b/", shouldExist: true},
},
wantErr: false,
wantArtifactSet: []*sourcev1.Artifact{
{Revision: "a"},
{Revision: "b"},
},
},
{
name: "Include get failure",
includes: []include{
{name: "a", toPath: "a/"},
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NotFound", "could not get resource for include 'a': gitrepositories.source.toolkit.fluxcd.io \"a\" not found"),
},
},
{
name: "Include without an artifact makes IncludeUnavailable=True",
dependencies: []dependency{
{
name: "a",
withArtifact: false,
conditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "Foo", "foo unavailable"),
},
},
},
includes: []include{
{name: "a", toPath: "a/"},
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NoArtifact", "no artifact available for include 'a'"),
},
},
{
name: "Outdated IncludeUnavailable is removed",
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NoArtifact", "")
},
assertConditions: []metav1.Condition{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
var depObjs []client.Object
for _, d := range tt.dependencies {
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: d.name,
},
Status: sourcev1.GitRepositoryStatus{
Conditions: d.conditions,
},
}
if d.withArtifact {
obj.Status.Artifact = &sourcev1.Artifact{
Path: d.name + ".tar.gz",
Revision: d.name,
LastUpdateTime: metav1.Now(),
}
}
depObjs = append(depObjs, obj)
}
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(testEnv.GetScheme()).
WithStatusSubresource(&sourcev1.GitRepository{})
if len(tt.dependencies) > 0 {
clientBuilder.WithObjects(depObjs...)
}
r := &GitRepositoryReconciler{
Client: clientBuilder.Build(),
EventRecorder: record.NewFakeRecorder(32),
patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"),
}
obj := &sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "reconcile-include",
},
Spec: sourcev1.GitRepositorySpec{
Interval: metav1.Duration{Duration: interval},
},
}
for i, incl := range tt.includes {
incl := sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: incl.name},
FromPath: incl.fromPath,
ToPath: incl.toPath,
}
tt.includes[i].fromPath = incl.GetFromPath()
tt.includes[i].toPath = incl.GetToPath()
obj.Spec.Include = append(obj.Spec.Include, incl)
}
gotArtifactSet, err := r.fetchIncludes(ctx, obj)
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(obj.GetConditions()).To(conditions.MatchConditions(tt.assertConditions))
if !tt.wantErr && gotArtifactSet != nil {
g.Expect(gotArtifactSet.Diff(tt.wantArtifactSet)).To(BeFalse())
}
})
}
}
func resetChmod(path string, dirMode os.FileMode, fileMode os.FileMode) error {
err := filepath.Walk(path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Mode() != dirMode {
os.Chmod(path, dirMode)
} else if !info.IsDir() && info.Mode() != fileMode {
os.Chmod(path, fileMode)
}
return nil
})
if err != nil {
return fmt.Errorf("cannot reset file permissions: %v", err)
}
return nil
}
func TestGitRepositoryIncludeEqual(t *testing.T) {
tests := []struct {
name string
a sourcev1.GitRepositoryInclude
b sourcev1.GitRepositoryInclude
want bool
}{
{
name: "empty",
want: true,
},
{
name: "different refs",
a: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
},
b: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "bar"},
},
want: false,
},
{
name: "same refs",
a: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
},
b: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
},
want: true,
},
{
name: "different from paths",
a: sourcev1.GitRepositoryInclude{FromPath: "foo"},
b: sourcev1.GitRepositoryInclude{FromPath: "bar"},
want: false,
},
{
name: "same from paths",
a: sourcev1.GitRepositoryInclude{FromPath: "foo"},
b: sourcev1.GitRepositoryInclude{FromPath: "foo"},
want: true,
},
{
name: "different to paths",
a: sourcev1.GitRepositoryInclude{ToPath: "foo"},
b: sourcev1.GitRepositoryInclude{ToPath: "bar"},
want: false,
},
{
name: "same to paths",
a: sourcev1.GitRepositoryInclude{ToPath: "foo"},
b: sourcev1.GitRepositoryInclude{ToPath: "foo"},
want: true,
},
{
name: "same all",
a: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo-ref"},
FromPath: "foo-path",
ToPath: "bar-path",
},
b: sourcev1.GitRepositoryInclude{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo-ref"},
FromPath: "foo-path",
ToPath: "bar-path",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
g.Expect(gitRepositoryIncludeEqual(tt.a, tt.b)).To(Equal(tt.want))
})
}
}
func TestGitContentConfigChanged(t *testing.T) {
tests := []struct {
name string
obj sourcev1.GitRepository
artifacts []*sourcev1.Artifact
want bool
}{
{
name: "no content config",
want: false,
},
{
name: "unobserved ignore",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{Ignore: ptr.To("foo")},
},
want: true,
},
{
name: "observed ignore",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{Ignore: ptr.To("foo")},
Status: sourcev1.GitRepositoryStatus{ObservedIgnore: ptr.To("foo")},
},
want: false,
},
{
name: "unobserved recurse submodules",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{RecurseSubmodules: true},
},
want: true,
},
{
name: "observed recurse submodules",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{RecurseSubmodules: true},
Status: sourcev1.GitRepositoryStatus{ObservedRecurseSubmodules: true},
},
want: false,
},
{
name: "unobserved include",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{GitRepositoryRef: meta.LocalObjectReference{Name: "foo"}, FromPath: "bar", ToPath: "baz"},
},
},
},
want: true,
},
{
name: "observed include",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Digest: "bbb"}},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
},
want: false,
},
{
name: "observed include but different artifact revision",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Digest: "bbb"}},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "ccc", Digest: "bbb"},
},
want: true,
},
{
name: "observed include but different artifact digest",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Digest: "bbb"}},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "ddd"},
},
want: true,
},
{
name: "observed include but updated spec",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{{Revision: "aaa", Digest: "bbb"}},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
},
want: true,
},
{
name: "different number of include and observed include",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
IncludedArtifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
{Revision: "ccc", Digest: "ccc"},
},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
{Revision: "ccc", Digest: "ddd"},
},
want: true,
},
{
name: "different number of include and artifactset",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
{Revision: "ccc", Digest: "ccc"},
},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
},
want: true,
},
{
name: "different number of include and included artifacts",
obj: sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Include: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
},
Status: sourcev1.GitRepositoryStatus{
ObservedInclude: []sourcev1.GitRepositoryInclude{
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo"},
FromPath: "bar",
ToPath: "baz",
},
{
GitRepositoryRef: meta.LocalObjectReference{Name: "foo2"},
FromPath: "bar",
ToPath: "baz",
},
},
IncludedArtifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
},
},
},
artifacts: []*sourcev1.Artifact{
{Revision: "aaa", Digest: "bbb"},
{Revision: "ccc", Digest: "ccc"},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
includes := artifactSet(tt.artifacts)
g.Expect(gitContentConfigChanged(&tt.obj, &includes)).To(Equal(tt.want))
})
}
}
func Test_requiresVerification(t *testing.T) {
tests := []struct {
name string
obj *sourcev1.GitRepository
want bool
}{
{
name: "GitRepository without verification does not require verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{},
},
want: false,
},
{
name: "GitRepository with verification and no observed verification mode in status requires verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{},
},
},
want: true,
},
{
name: "GitRepository with HEAD verification and a verified tag requires verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTag),
},
},
want: true,
},
{
name: "GitRepository with tag and HEAD verification and a verified tag requires verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTagAndHEAD,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTag),
},
},
want: true,
},
{
name: "GitRepository with tag verification and a verified HEAD requires verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD),
},
},
want: true,
},
{
name: "GitRepository with tag and HEAD verification and a verified HEAD requires verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTagAndHEAD,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitHEAD),
},
},
want: true,
},
{
name: "GitRepository with tag verification and a verified HEAD and tag does not require verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitTag,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD),
},
},
want: false,
},
{
name: "GitRepository with head verification and a verified HEAD and tag does not require verification",
obj: &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
Verification: &sourcev1.GitRepositoryVerification{
Mode: sourcev1.ModeGitHEAD,
},
},
Status: sourcev1.GitRepositoryStatus{
SourceVerificationMode: ptrToVerificationMode(sourcev1.ModeGitTagAndHEAD),
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
verificationRequired := requiresVerification(tt.obj)
g.Expect(verificationRequired).To(Equal(tt.want))
})
}
}
func ptrToVerificationMode(mode sourcev1.GitVerificationMode) *sourcev1.GitVerificationMode {
return &mode
}