diff --git a/controllers/gitrepository_controller_test.go b/controllers/gitrepository_controller_test.go new file mode 100644 index 00000000..6cdd16e7 --- /dev/null +++ b/controllers/gitrepository_controller_test.go @@ -0,0 +1,205 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 controllers + +import ( + "context" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/fluxcd/source-controller/internal/testserver" +) + +var _ = Describe("GitRepositoryReconciler", func() { + + const ( + timeout = time.Second * 30 + interval = time.Second * 1 + indexInterval = time.Second * 1 + ) + + Context("GitRepsoitory", func() { + var ( + namespace *corev1.Namespace + gitServer *testserver.GitServer + err error + ) + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "git-repository-test" + randStringRunes(5)}, + } + err = k8sClient.Create(context.Background(), namespace) + Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + gitServer, err = testserver.NewTempGitServer() + Expect(err).NotTo(HaveOccurred()) + gitServer.AutoCreate() + }) + + AfterEach(func() { + os.RemoveAll(gitServer.Root()) + + err = k8sClient.Delete(context.Background(), namespace) + Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") + }) + + It("Creates artifacts for", func() { + err = gitServer.StartHTTP() + Expect(err).NotTo(HaveOccurred()) + + By("Creating a new git repository with a single commit") + u, err := url.Parse(gitServer.HTTPAddress()) + Expect(err).NotTo(HaveOccurred()) + u.Path = path.Join(u.Path, "repository.git") + + fs := memfs.New() + r, err := git.Init(memory.NewStorage(), fs) + Expect(err).NotTo(HaveOccurred()) + + _, err = r.CreateRemote(&config.RemoteConfig{ + Name: "origin", + URLs: []string{u.String()}, + }) + Expect(err).NotTo(HaveOccurred()) + + ff, err := fs.Create("fixture") + Expect(err).NotTo(HaveOccurred()) + _ = ff.Close() + + wt, err := r.Worktree() + Expect(err).NotTo(HaveOccurred()) + + _, err = wt.Add(fs.Join("fixture")) + Expect(err).NotTo(HaveOccurred()) + + cHash, err := wt.Commit("Sample", &git.CommitOptions{Author: &object.Signature{ + Name: "John Doe", + Email: "john@example.com", + When: time.Now(), + }}) + Expect(err).NotTo(HaveOccurred()) + + err = r.Push(&git.PushOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a new resource for the repository") + key := types.NamespacedName{ + Name: "gitrepository-sample-" + randStringRunes(5), + Namespace: namespace.Name, + } + created := &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: sourcev1.GitRepositorySpec{ + URL: u.String(), + Interval: metav1.Duration{Duration: indexInterval}, + }, + } + Expect(k8sClient.Create(context.Background(), created)).Should(Succeed()) + + By("Expecting artifact and revision") + got := &sourcev1.GitRepository{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact != nil && storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + Expect(got.Status.Artifact.Revision).To(Equal("master/" + cHash.String())) + + By("Pushing a change to the repository") + ff, err = fs.Create("fixture2") + Expect(err).NotTo(HaveOccurred()) + _ = ff.Close() + + _, err = wt.Add(fs.Join("fixture2")) + Expect(err).NotTo(HaveOccurred()) + + cHash, err = wt.Commit("Sample", &git.CommitOptions{Author: &object.Signature{ + Name: "John Doe", + Email: "john@example.com", + When: time.Now(), + }}) + Expect(err).NotTo(HaveOccurred()) + + err = r.Push(&git.PushOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Expecting new artifact revision and GC") + Eventually(func() bool { + now := &sourcev1.GitRepository{} + _ = k8sClient.Get(context.Background(), key, now) + return now.Status.Artifact.Revision != got.Status.Artifact.Revision && + !storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + + By("Expecting git clone error") + updated := &sourcev1.GitRepository{} + Expect(k8sClient.Get(context.Background(), key, updated)).Should(Succeed()) + updated.Spec.URL = "https://invalid.com" + Expect(k8sClient.Update(context.Background(), updated)).Should(Succeed()) + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, updated) + for _, c := range updated.Status.Conditions { + if c.Reason == sourcev1.GitOperationFailedReason && + strings.Contains(c.Message, "git clone error") { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + Expect(updated.Status.Artifact).ToNot(BeNil()) + + By("Expecting to delete successfully") + got = &sourcev1.GitRepository{} + Eventually(func() error { + _ = k8sClient.Get(context.Background(), key, got) + return k8sClient.Delete(context.Background(), got) + }, timeout, interval).Should(Succeed()) + + By("Expecting delete to finish") + Eventually(func() error { + return k8sClient.Get(context.Background(), key, &sourcev1.GitRepository{}) + }).ShouldNot(Succeed()) + + By("Expecting GC on delete") + exists := func(path string) bool { + // wait for tmp sync on macOS + time.Sleep(time.Second) + _, err := os.Stat(path) + return err == nil + } + Eventually(exists(got.Status.Artifact.Path), timeout, interval).ShouldNot(BeTrue()) + }) + }) +}) diff --git a/controllers/storage.go b/controllers/storage.go index 3eb699bb..5b751698 100644 --- a/controllers/storage.go +++ b/controllers/storage.go @@ -94,7 +94,7 @@ func (s *Storage) RemoveAll(artifact sourcev1.Artifact) error { // RemoveAllButCurrent removes all files for the given artifact base dir excluding the current one func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) error { dir := filepath.Dir(artifact.Path) - errors := []string{} + var errors []string _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if path != artifact.Path && !info.IsDir() && info.Mode()&os.ModeSymlink != os.ModeSymlink { if err := os.Remove(path); err != nil { diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 943ea157..148ccc92 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -94,7 +94,7 @@ var _ = BeforeSuite(func(done Done) { Expect(loadExampleKeys()).To(Succeed()) - tmpStoragePath, err := ioutil.TempDir("", "helmrepository") + tmpStoragePath, err := ioutil.TempDir("", "source-controller-storage-") Expect(err).NotTo(HaveOccurred(), "failed to create tmp storage dir") storage, err = NewStorage(tmpStoragePath, "localhost", time.Second*30) @@ -105,6 +105,14 @@ var _ = BeforeSuite(func(done Done) { }) Expect(err).ToNot(HaveOccurred()) + err = (&GitRepositoryReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("GitRepository"), + Scheme: scheme.Scheme, + Storage: storage, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred(), "failed to setup GtRepositoryReconciler") + err = (&HelmRepositoryReconciler{ Client: k8sManager.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("HelmRepository"), diff --git a/go.mod b/go.mod index 3040f457..c5af2e34 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/blang/semver v3.5.0+incompatible + github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 github.com/go-logr/logr v0.1.0 github.com/onsi/ginkgo v1.11.0 diff --git a/internal/testserver/git.go b/internal/testserver/git.go index 820dad33..60677dcb 100644 --- a/internal/testserver/git.go +++ b/internal/testserver/git.go @@ -55,6 +55,13 @@ type GitServer struct { sshServer *gitkit.SSH } +// AutoCreate enables the automatic creation of a non-existing Git +// repository on push. +func (s *GitServer) AutoCreate() *GitServer { + s.config.AutoCreate = true + return s +} + // StartHTTP starts a new HTTP git server with the current configuration. func (s *GitServer) StartHTTP() error { s.StopHTTP()