diff --git a/api/v1alpha1/function_types.go b/api/v1alpha1/function_types.go index 60b2c87..cf6c424 100644 --- a/api/v1alpha1/function_types.go +++ b/api/v1alpha1/function_types.go @@ -77,6 +77,14 @@ type FunctionStatus struct { Runtime string `json:"runtime"` Conditions []metav1.Condition `json:"conditions,omitempty"` + + Git FunctionStatusGit `json:"git,omitempty"` +} + +type FunctionStatusGit struct { + ResolvedBranch string `json:"resolvedBranch,omitempty"` + ObservedCommit string `json:"observedCommit,omitempty"` + LastChecked metav1.Time `json:"lastChecked,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8962b2a..4a934d6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -157,6 +157,7 @@ func (in *FunctionStatus) DeepCopyInto(out *FunctionStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.Git.DeepCopyInto(&out.Git) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionStatus. @@ -168,3 +169,19 @@ func (in *FunctionStatus) DeepCopy() *FunctionStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FunctionStatusGit) DeepCopyInto(out *FunctionStatusGit) { + *out = *in + in.LastChecked.DeepCopyInto(&out.LastChecked) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionStatusGit. +func (in *FunctionStatusGit) DeepCopy() *FunctionStatusGit { + if in == nil { + return nil + } + out := new(FunctionStatusGit) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/functions.dev_functions.yaml b/config/crd/bases/functions.dev_functions.yaml index c128fac..2bb1f5a 100644 --- a/config/crd/bases/functions.dev_functions.yaml +++ b/config/crd/bases/functions.dev_functions.yaml @@ -165,6 +165,16 @@ spec: - type type: object type: array + git: + properties: + lastChecked: + format: date-time + type: string + observedCommit: + type: string + resolvedBranch: + type: string + type: object name: type: string runtime: diff --git a/internal/controller/function_controller.go b/internal/controller/function_controller.go index d060c9a..88f4310 100644 --- a/internal/controller/function_controller.go +++ b/internal/controller/function_controller.go @@ -121,6 +121,8 @@ func (r *FunctionReconciler) reconcile(ctx context.Context, function *v1alpha1.F } defer repo.Cleanup() + r.updateFunctionStatusGit(function, repo) + if err := r.ensureDeployment(ctx, function, repo, metadata); err != nil { return fmt.Errorf("deploying function failed: %w", err) } @@ -442,3 +444,10 @@ func (r *FunctionReconciler) isMiddlewareLatest(ctx context.Context, metadata *f return latestMiddleware == functionMiddleware, nil } + +// updateFunctionStatusGit updates the functions status with the Git information +func (r *FunctionReconciler) updateFunctionStatusGit(function *v1alpha1.Function, repo *git.Repository) { + function.Status.Git.ResolvedBranch = repo.Branch + function.Status.Git.ObservedCommit = repo.Commit + function.Status.Git.LastChecked = metav1.Now() +} diff --git a/internal/controller/function_controller_test.go b/internal/controller/function_controller_test.go index 14f0295..bb388f8 100644 --- a/internal/controller/function_controller_test.go +++ b/internal/controller/function_controller_test.go @@ -75,6 +75,7 @@ var _ = Describe("Function Controller", func() { type reconcileTestCase struct { spec functionsdevv1alpha1.FunctionSpec configureMocks func(*funccli.MockManager, *git.MockManager) + statusChecks func(*functionsdevv1alpha1.FunctionStatus) } DescribeTable("should successfully reconcile the resource", @@ -101,6 +102,14 @@ var _ = Describe("Function Controller", func() { NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) + + if tc.statusChecks != nil { + f := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, typeNamespacedName, f) + Expect(err).NotTo(HaveOccurred()) + + tc.statusChecks(&f.Status) + } }, Entry("should deploy when middleware update required", reconcileTestCase{ spec: defaultSpec, @@ -149,6 +158,30 @@ var _ = Describe("Function Controller", func() { gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "main", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil) }, }), + + Entry("should contain the git information in the status", reconcileTestCase{ + spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: "https://github.com/foo/bar", + Branch: "my-branch", + }, + }, + configureMocks: func(funcMock *funccli.MockManager, gitMock *git.MockManager) { + funcMock.EXPECT().Describe(mock.Anything, functionName, resourceNamespace).Return(functions.Instance{ + Middleware: functions.Middleware{ + Version: "v1.0.0", + }, + }, nil) + funcMock.EXPECT().GetLatestMiddlewareVersion(mock.Anything, mock.Anything, mock.Anything).Return("v1.0.0", nil) + funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil) + + gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "my-branch", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}, WithRepoOptionBranch("my-branch"), WithRepoOptionCommit("foobar")), nil) + }, + statusChecks: func(status *functionsdevv1alpha1.FunctionStatus) { + Expect(status.Git.ResolvedBranch).Should(Equal("my-branch")) + Expect(status.Git.ObservedCommit).Should(Equal("foobar")) + }, + }), ) }) }) @@ -165,7 +198,9 @@ func createFunctionResource(name, namespace string, spec functionsdevv1alpha1.Fu return k8sClient.Create(ctx, &resource) } -func createTmpGitRepo(function functions.Function) *git.Repository { +type RepoOption func(*git.Repository) + +func createTmpGitRepo(function functions.Function, repoOptions ...RepoOption) *git.Repository { tempDir, err := os.MkdirTemp("", function.Name) Expect(err).NotTo(HaveOccurred()) @@ -176,8 +211,31 @@ func createTmpGitRepo(function functions.Function) *git.Repository { err = os.WriteFile(funcYamlPath, f, 0644) Expect(err).NotTo(HaveOccurred()) - return &git.Repository{ + opts := &git.Repository{ CloneDir: tempDir, - SubPath: ".", + } + + for _, repoOption := range repoOptions { + repoOption(opts) + } + + return opts +} + +func WithRepoOptionSubPath(subPath string) RepoOption { + return func(repo *git.Repository) { + repo.SubPath = subPath + } +} + +func WithRepoOptionBranch(branch string) RepoOption { + return func(repo *git.Repository) { + repo.Branch = branch + } +} + +func WithRepoOptionCommit(commit string) RepoOption { + return func(repo *git.Repository) { + repo.Commit = commit } } diff --git a/internal/git/manager.go b/internal/git/manager.go index f9f48b2..5113b39 100644 --- a/internal/git/manager.go +++ b/internal/git/manager.go @@ -44,7 +44,7 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref return nil, fmt.Errorf("failed to create temporary directory: %w", err) } - _, err = git.PlainCloneContext(ctx, targetDir, &git.CloneOptions{ + repo, err := git.PlainCloneContext(ctx, targetDir, &git.CloneOptions{ URL: repoUrl, ReferenceName: plumbing.ReferenceName(reference), SingleBranch: true, @@ -55,9 +55,16 @@ func (m *managerImpl) CloneRepository(ctx context.Context, repoUrl, subPath, ref return nil, fmt.Errorf("failed to clone repo: %w", err) } + head, err := repo.Head() + if err != nil { + return nil, fmt.Errorf("failed to find head: %w", err) + } + return &Repository{ CloneDir: targetDir, SubPath: subPath, + Commit: head.Hash().String(), + Branch: reference, }, nil } diff --git a/internal/git/repository.go b/internal/git/repository.go index 67c0b0f..9058d48 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -8,6 +8,8 @@ import ( type Repository struct { CloneDir string SubPath string + Commit string + Branch string } func (r *Repository) Path() string {