diff --git a/api/v1alpha1/custom_package_types.go b/api/v1alpha1/custom_package_types.go index 58f4886f..04620c6c 100644 --- a/api/v1alpha1/custom_package_types.go +++ b/api/v1alpha1/custom_package_types.go @@ -23,18 +23,17 @@ type CustomPackageList struct { // CustomPackageSpec controls the installation of the custom applications. type CustomPackageSpec struct { - // Replicate specifies whether to replicate remote or local contents to the local gitea server. - // +kubebuilder:default:=false - Replicate bool `json:"replicate"` + ArgoCD ArgoCDPackageSpec `json:"argoCD,omitempty"` // GitServerURL specifies the base URL for the git server for API calls. // for example, https://gitea.cnoe.localtest.me:8443 - GitServerURL string `json:"gitServerURL"` + GitServerURL string `json:"gitServerURL"` + GitServerAuthSecretRef SecretReference `json:"gitServerAuthSecretRef"` // InternalGitServeURL specifies the base URL for the git server accessible within the cluster. // for example, http://my-gitea-http.gitea.svc.cluster.local:3000 - InternalGitServeURL string `json:"internalGitServeURL"` - GitServerAuthSecretRef SecretReference `json:"gitServerAuthSecretRef"` - - ArgoCD ArgoCDPackageSpec `json:"argoCD,omitempty"` + InternalGitServeURL string `json:"internalGitServeURL"` + // Replicate specifies whether to replicate remote or local contents to the local gitea server. + // +kubebuilder:default:=false + Replicate bool `json:"replicate"` } type ArgoCDPackageSpec struct { diff --git a/api/v1alpha1/gitrepository_types.go b/api/v1alpha1/gitrepository_types.go index dfd339dd..2eb75035 100644 --- a/api/v1alpha1/gitrepository_types.go +++ b/api/v1alpha1/gitrepository_types.go @@ -5,7 +5,6 @@ import ( ) type GitRepositorySpec struct { - Source GitRepositorySource `json:"source,omitempty"` // GitURL is the base URL of Git server used for API calls. // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=`^https?:\/\/.+$` @@ -14,7 +13,8 @@ type GitRepositorySpec struct { InternalGitURL string `json:"internalGitURL"` // SecretRef is the reference to secret that contain Git server credentials // +kubebuilder:validation:Optional - SecretRef SecretReference `json:"secretRef"` + SecretRef SecretReference `json:"secretRef"` + Source GitRepositorySource `json:"source,omitempty"` } type GitRepositorySource struct { @@ -54,9 +54,8 @@ type GitRepositoryStatus struct { InternalGitRepositoryUrl string `json:"internalGitRepositoryUrl"` // Path is the path within the repository that contains the files. // +kubebuilder:validation:Optional - Path string `json:"path"` - - Synced bool `json:"synced"` + Path string `json:"path"` + Synced bool `json:"synced"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/localbuild_types.go b/api/v1alpha1/localbuild_types.go index 949942eb..3dd93281 100644 --- a/api/v1alpha1/localbuild_types.go +++ b/api/v1alpha1/localbuild_types.go @@ -7,6 +7,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // LastObservedCLIStartTimeAnnotation indicates when the controller acted on a resource. + LastObservedCLIStartTimeAnnotation = "cnoe.io/last-observed-cli-start-time" + // CliStartTimeAnnotation indicates when the CLI was invoked. + CliStartTimeAnnotation = "cnoe.io/cli-start-time" + FieldManager = "idpbuilder" +) + // ArgoPackageConfigSpec Allows for configuration of the ArgoCD Installation. // If no fields are specified then the binary embedded resources will be used to intall ArgoCD. type ArgoPackageConfigSpec struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ccafd57e..8f7e5544 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -147,8 +147,8 @@ func (in *CustomPackageList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomPackageSpec) DeepCopyInto(out *CustomPackageSpec) { *out = *in - out.GitServerAuthSecretRef = in.GitServerAuthSecretRef out.ArgoCD = in.ArgoCD + out.GitServerAuthSecretRef = in.GitServerAuthSecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPackageSpec. @@ -288,8 +288,8 @@ func (in *GitRepositorySource) DeepCopy() *GitRepositorySource { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRepositorySpec) DeepCopyInto(out *GitRepositorySpec) { *out = *in - out.Source = in.Source out.SecretRef = in.SecretRef + out.Source = in.Source } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepositorySpec. diff --git a/pkg/build/build.go b/pkg/build/build.go index 4a5b5015..18862b32 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -3,7 +3,6 @@ package build import ( "context" "fmt" - "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/controllers" @@ -16,6 +15,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/manager" + "time" ) var ( @@ -142,15 +142,20 @@ func (b *Build) Run(ctx context.Context, recreateCluster bool) error { return err } - // Create localbuild resource localBuild := v1alpha1.Localbuild{ ObjectMeta: metav1.ObjectMeta{ Name: b.name, }, } + cliStartTime := time.Now().Format(time.RFC3339Nano) + setupLog.Info("Creating localbuild resource") _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, &localBuild, func() error { + if localBuild.ObjectMeta.Annotations == nil { + localBuild.ObjectMeta.Annotations = map[string]string{} + } + localBuild.ObjectMeta.Annotations[v1alpha1.CliStartTimeAnnotation] = cliStartTime localBuild.Spec = v1alpha1.LocalbuildSpec{ PackageConfigs: v1alpha1.PackageConfigsSpec{ Argo: v1alpha1.ArgoPackageConfigSpec{ @@ -160,7 +165,6 @@ func (b *Build) Run(ctx context.Context, recreateCluster bool) error { Enabled: true, }, GitConfig: v1alpha1.GitConfigSpec{ - // hint: for the old behavior, replace Type value below with globals.GitServerResourcename() Type: globals.GiteaResourceName(), }, CustomPackageDirs: b.customPackageDirs, diff --git a/pkg/controllers/custompackage/controller.go b/pkg/controllers/custompackage/controller.go index adb24375..92026fa3 100644 --- a/pkg/controllers/custompackage/controller.go +++ b/pkg/controllers/custompackage/controller.go @@ -3,6 +3,7 @@ package custompackage import ( "context" "fmt" + "github.com/cnoe-io/idpbuilder/pkg/util" "os" "path/filepath" "strings" @@ -54,10 +55,16 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu func (r *Reconciler) postProcessReconcile(ctx context.Context, req ctrl.Request, pkg *v1alpha1.CustomPackage) { logger := log.FromContext(ctx) + err := r.Status().Update(ctx, pkg) if err != nil { logger.Error(err, "failed updating repo status") } + + err = util.UpdateSyncAnnotation(ctx, r.Client, pkg) + if err != nil { + logger.Error(err, "failed updating repo annotation") + } } // create an in-cluster repository CR, update the application spec, then apply @@ -173,7 +180,21 @@ func (r *Reconciler) reconcileGitRepo(ctx context.Context, resource *v1alpha1.Cu Name: repoName, Namespace: resource.Namespace, }, - Spec: v1alpha1.GitRepositorySpec{ + } + + cliStartTime, _ := util.GetCLIStartTimeAnnotationValue(resource.ObjectMeta.Annotations) + + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error { + if err := controllerutil.SetControllerReference(resource, repo, r.Scheme); err != nil { + return err + } + + if repo.ObjectMeta.Annotations == nil { + repo.ObjectMeta.Annotations = make(map[string]string) + } + util.SetCLIStartTimeAnnotationValue(repo.ObjectMeta.Annotations, cliStartTime) + + repo.Spec = v1alpha1.GitRepositorySpec{ Source: v1alpha1.GitRepositorySource{ Type: "local", Path: absPath, @@ -181,13 +202,8 @@ func (r *Reconciler) reconcileGitRepo(ctx context.Context, resource *v1alpha1.Cu GitURL: resource.Spec.GitServerURL, InternalGitURL: resource.Spec.InternalGitServeURL, SecretRef: resource.Spec.GitServerAuthSecretRef, - }, - } - - _, err := controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error { - if err := controllerutil.SetControllerReference(resource, repo, r.Scheme); err != nil { - return err } + return nil }) // it's possible for an application to specify the same directory multiple times in the spec. diff --git a/pkg/controllers/gitrepository/controller.go b/pkg/controllers/gitrepository/controller.go index 1fe9b7fc..a79839c4 100644 --- a/pkg/controllers/gitrepository/controller.go +++ b/pkg/controllers/gitrepository/controller.go @@ -97,13 +97,12 @@ func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, client.IgnoreNotFound(err) } + defer r.postProcessReconcile(ctx, req, &gitRepo) if !r.shouldProcess(gitRepo) { return ctrl.Result{Requeue: false}, nil } logger.Info("reconciling GitRepository", "name", req.Name, "namespace", req.Namespace) - defer r.postProcessReconcile(ctx, req, &gitRepo) - result, err := r.reconcileGitRepo(ctx, &gitRepo) if err != nil { r.Recorder.Event(&gitRepo, "Warning", "reconcile error", err.Error()) @@ -116,16 +115,22 @@ func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) func (r *RepositoryReconciler) postProcessReconcile(ctx context.Context, req ctrl.Request, repo *v1alpha1.GitRepository) { logger := log.FromContext(ctx) + err := r.Status().Update(ctx, repo) if err != nil { logger.Error(err, "failed updating repo status") } + + err = util.UpdateSyncAnnotation(ctx, r.Client, repo) + if err != nil { + logger.Error(err, "failed updating repo annotation") + } } func (r *RepositoryReconciler) reconcileGitRepo(ctx context.Context, repo *v1alpha1.GitRepository) (ctrl.Result, error) { logger := log.FromContext(ctx) logger.Info("reconciling", "name", repo.Name, "dir", repo.Spec.Source) - + repo.Status.Synced = false tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } @@ -147,13 +152,14 @@ func (r *RepositoryReconciler) reconcileGitRepo(ctx context.Context, repo *v1alp if err != nil { return ctrl.Result{Requeue: true, RequeueAfter: requeueTime}, fmt.Errorf("failed to create or update repo %w", err) } - repo.Status.ExternalGitRepositoryUrl = giteaRepo.CloneURL - repo.Status.InternalGitRepositoryUrl = getRepositoryURL(repo.Namespace, repo.Name, repo.Spec.InternalGitURL) err = r.reconcileRepoContent(ctx, repo, giteaRepo) if err != nil { return ctrl.Result{Requeue: true, RequeueAfter: requeueTime}, fmt.Errorf("failed to reconcile repo content %w", err) } + + repo.Status.ExternalGitRepositoryUrl = giteaRepo.CloneURL + repo.Status.InternalGitRepositoryUrl = getRepositoryURL(repo.Namespace, repo.Name, repo.Spec.InternalGitURL) repo.Status.Synced = true return ctrl.Result{Requeue: true, RequeueAfter: requeueTime}, nil } diff --git a/pkg/controllers/gitrepository/controller_test.go b/pkg/controllers/gitrepository/controller_test.go index 5799e582..0fac21c6 100644 --- a/pkg/controllers/gitrepository/controller_test.go +++ b/pkg/controllers/gitrepository/controller_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + ctrl "sigs.k8s.io/controller-runtime" "testing" "time" @@ -60,6 +61,7 @@ type testCase struct { type fakeClient struct { client.Client + patchObj client.Object } func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { @@ -71,6 +73,23 @@ func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.O return nil } +func (f *fakeClient) Status() client.StatusWriter { + return fakeStatusWriter{} +} + +func (f *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + f.patchObj = obj + return nil +} + +type fakeStatusWriter struct { + client.StatusWriter +} + +func (f fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil +} + func setUpLocalRepo() (string, string, error) { repoDir, err := os.MkdirTemp("", fmt.Sprintf("test")) if err != nil { @@ -377,6 +396,41 @@ func TestGitRepositoryReconcile(t *testing.T) { if !reflect.DeepEqual(v.input.Status, v.expect.resource) { t.Fatalf("objects not equal") } + }) } } + +func TestGitRepositoryPostReconcile(t *testing.T) { + c := fakeClient{} + reconciler := RepositoryReconciler{ + Client: &c, + } + testTime := time.Now().Format(time.RFC3339Nano) + repo := v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Annotations: map[string]string{ + v1alpha1.CliStartTimeAnnotation: testTime, + }, + }, + } + + reconciler.postProcessReconcile(context.Background(), ctrl.Request{}, &repo) + annotations := c.patchObj.GetAnnotations() + v, ok := annotations[v1alpha1.LastObservedCLIStartTimeAnnotation] + if !ok { + t.Fatalf("expected annotation not found: %s", v1alpha1.LastObservedCLIStartTimeAnnotation) + } + if v != testTime { + t.Fatalf("annotation values does not match") + } + + repo.Annotations[v1alpha1.LastObservedCLIStartTimeAnnotation] = "abc" + reconciler.postProcessReconcile(context.Background(), ctrl.Request{}, &repo) + v = annotations[v1alpha1.LastObservedCLIStartTimeAnnotation] + if v != testTime { + t.Fatalf("annotation values does not match") + } +} diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index a303ad62..fbb55b8c 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -3,6 +3,7 @@ package localbuild import ( "context" "fmt" + "github.com/cnoe-io/idpbuilder/pkg/util" "os" "path/filepath" "strings" @@ -208,14 +209,31 @@ func (r *LocalbuildReconciler) shouldShutDown(ctx context.Context, resource *v1a return false, nil } + cliStartTime, err := util.GetCLIStartTimeAnnotationValue(resource.Annotations) + if err != nil { + return true, err + } + repos := &v1alpha1.GitRepositoryList{} - err := r.Client.List(ctx, repos, client.InNamespace(resource.Namespace)) + err = r.Client.List(ctx, repos, client.InNamespace(resource.Namespace)) if err != nil { return false, fmt.Errorf("listing repositories %w", err) } for i := range repos.Items { repo := repos.Items[i] - if !repo.Status.Synced { + + _, gErr := util.GetCLIStartTimeAnnotationValue(repo.ObjectMeta.Annotations) + if gErr != nil { + // this means this repository resource is not managed by localbuild + continue + } + + observedTime, gErr := util.GetLastObservedSyncTimeAnnotationValue(repo.ObjectMeta.Annotations) + if gErr != nil { + return false, gErr + } + + if !repo.Status.Synced || cliStartTime != observedTime { return false, nil } } @@ -228,7 +246,17 @@ func (r *LocalbuildReconciler) shouldShutDown(ctx context.Context, resource *v1a for i := range pkgs.Items { pkg := pkgs.Items[i] - if !pkg.Status.Synced { + _, gErr := util.GetCLIStartTimeAnnotationValue(pkg.ObjectMeta.Annotations) + if gErr != nil { + continue + } + + observedTime, gErr := util.GetLastObservedSyncTimeAnnotationValue(pkg.ObjectMeta.Annotations) + if gErr != nil { + return false, gErr + } + + if !pkg.Status.Synced || cliStartTime != observedTime { return false, nil } } @@ -270,7 +298,24 @@ func (r *LocalbuildReconciler) reconcileCustomPkg(ctx context.Context, resource Name: getCustomPackageName(file.Name(), appName), Namespace: globals.GetProjectNamespace(resource.Name), }, - Spec: v1alpha1.CustomPackageSpec{ + } + + cliStartTime, err := util.GetCLIStartTimeAnnotationValue(resource.ObjectMeta.Annotations) + if err != nil { + logger.Error(err, "this resource may not sync correctly") + } + + _, fErr = controllerutil.CreateOrUpdate(ctx, r.Client, customPkg, func() error { + if err := controllerutil.SetControllerReference(resource, customPkg, r.Scheme); err != nil { + return err + } + if customPkg.ObjectMeta.Annotations == nil { + customPkg.ObjectMeta.Annotations = make(map[string]string) + } + + util.SetCLIStartTimeAnnotationValue(customPkg.ObjectMeta.Annotations, cliStartTime) + + customPkg.Spec = v1alpha1.CustomPackageSpec{ Replicate: true, GitServerURL: resource.Status.Gitea.ExternalURL, InternalGitServeURL: resource.Status.Gitea.InternalURL, @@ -283,13 +328,6 @@ func (r *LocalbuildReconciler) reconcileCustomPkg(ctx context.Context, resource Name: appName, Namespace: appNS, }, - }, - Status: v1alpha1.CustomPackageStatus{}, - } - - _, fErr = controllerutil.CreateOrUpdate(ctx, r.Client, customPkg, func() error { - if err := controllerutil.SetControllerReference(resource, customPkg, r.Scheme); err != nil { - return err } return nil }) @@ -309,7 +347,24 @@ func (r *LocalbuildReconciler) reconcileGitRepo(ctx context.Context, resource *v Name: repoName, Namespace: globals.GetProjectNamespace(resource.Name), }, - Spec: v1alpha1.GitRepositorySpec{ + } + + cliStartTime, err := util.GetCLIStartTimeAnnotationValue(resource.Annotations) + if err != nil { + return nil, err + } + + _, err = controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error { + if err := controllerutil.SetControllerReference(resource, repo, r.Scheme); err != nil { + return err + } + + if repo.ObjectMeta.Annotations == nil { + repo.ObjectMeta.Annotations = make(map[string]string) + } + util.SetCLIStartTimeAnnotationValue(repo.ObjectMeta.Annotations, cliStartTime) + + repo.Spec = v1alpha1.GitRepositorySpec{ Source: v1alpha1.GitRepositorySource{ Type: repoType, }, @@ -319,19 +374,14 @@ func (r *LocalbuildReconciler) reconcileGitRepo(ctx context.Context, resource *v Name: resource.Status.Gitea.AdminUserSecretName, Namespace: resource.Status.Gitea.AdminUserSecretNamespace, }, - }, - } - - if repoType == "embedded" { - repo.Spec.Source.EmbeddedAppName = embeddedName - } else { - repo.Spec.Source.Path = absPath - } + } - _, err := controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error { - if err := controllerutil.SetControllerReference(resource, repo, r.Scheme); err != nil { - return err + if repoType == "embedded" { + repo.Spec.Source.EmbeddedAppName = embeddedName + } else { + repo.Spec.Source.Path = absPath } + return nil }) diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 00000000..b0a4011c --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,60 @@ +package util + +import ( + "context" + "fmt" + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetCLIStartTimeAnnotationValue(annotations map[string]string) (string, error) { + if annotations == nil { + return "", fmt.Errorf("this object's annotation is nil") + } + timeStamp, ok := annotations[v1alpha1.CliStartTimeAnnotation] + if ok { + return timeStamp, nil + } + return "", fmt.Errorf("expected annotation, %s, not found", v1alpha1.CliStartTimeAnnotation) +} + +func SetCLIStartTimeAnnotationValue(annotations map[string]string, timeStamp string) { + if timeStamp != "" && annotations != nil { + annotations[v1alpha1.CliStartTimeAnnotation] = timeStamp + } +} + +func SetLastObservedSyncTimeAnnotationValue(annotations map[string]string, timeStamp string) { + if timeStamp != "" && annotations != nil { + annotations[v1alpha1.LastObservedCLIStartTimeAnnotation] = timeStamp + } +} + +func GetLastObservedSyncTimeAnnotationValue(annotations map[string]string) (string, error) { + if annotations == nil { + return "", fmt.Errorf("this object's annotation is nil") + } + timeStamp, ok := annotations[v1alpha1.LastObservedCLIStartTimeAnnotation] + if ok { + return timeStamp, nil + } + return "", fmt.Errorf("expected annotation, %s, not found", v1alpha1.LastObservedCLIStartTimeAnnotation) +} + +func UpdateSyncAnnotation(ctx context.Context, kubeClient client.Client, obj client.Object) error { + timeStamp, err := GetCLIStartTimeAnnotationValue(obj.GetAnnotations()) + if err != nil { + return err + } + annotations := make(map[string]string, 1) + SetLastObservedSyncTimeAnnotationValue(annotations, timeStamp) + // MUST be unstructured to avoid managing fields we do not care about. + u := unstructured.Unstructured{} + u.SetAnnotations(annotations) + u.SetName(obj.GetName()) + u.SetNamespace(obj.GetNamespace()) + u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) + + return kubeClient.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager)) +}