diff --git a/cyclops-ctrl/api/v1alpha1/module_types.go b/cyclops-ctrl/api/v1alpha1/module_types.go index 3789d0a36..3466c341c 100644 --- a/cyclops-ctrl/api/v1alpha1/module_types.go +++ b/cyclops-ctrl/api/v1alpha1/module_types.go @@ -43,6 +43,10 @@ const ( TemplateSourceTypeGit TemplateSourceType = "git" TemplateSourceTypeHelm TemplateSourceType = "helm" TemplateSourceTypeOCI TemplateSourceType = "oci" + + GitOpsWriteRepoAnnotation = "cyclops-ui.com/write-repo" + GitOpsWritePathAnnotation = "cyclops-ui.com/write-path" + GitOpsWriteRevisionAnnotation = "cyclops-ui.com/write-revision" ) type TemplateRef struct { @@ -88,12 +92,12 @@ type GroupVersionResource struct { // ModuleStatus defines the observed state of Module type ModuleStatus struct { - ReconciliationStatus ReconciliationStatus `json:"reconciliationStatus"` - TemplateResolvedVersion string `json:"templateResolvedVersion"` + ReconciliationStatus *ReconciliationStatus `json:"reconciliationStatus,omitempty"` + TemplateResolvedVersion string `json:"templateResolvedVersion,omitempty"` // +kubebuilder:validation:Optional - ManagedGVRs []GroupVersionResource `json:"managedGVRs"` + ManagedGVRs []GroupVersionResource `json:"managedGVRs,omitempty"` // +kubebuilder:validation:Optional - IconURL string `json:"iconURL"` + IconURL string `json:"iconURL,omitempty"` } type HistoryTemplateRef struct { diff --git a/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go b/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go index 0bce7172a..633221cd0 100644 --- a/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go +++ b/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go @@ -157,7 +157,11 @@ func (in *ModuleSpec) DeepCopy() *ModuleSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ModuleStatus) DeepCopyInto(out *ModuleStatus) { *out = *in - in.ReconciliationStatus.DeepCopyInto(&out.ReconciliationStatus) + if in.ReconciliationStatus != nil { + in, out := &in.ReconciliationStatus, &out.ReconciliationStatus + *out = new(ReconciliationStatus) + (*in).DeepCopyInto(*out) + } if in.ManagedGVRs != nil { in, out := &in.ManagedGVRs, &out.ManagedGVRs *out = make([]GroupVersionResource, len(*in)) diff --git a/cyclops-ctrl/cmd/main/main.go b/cyclops-ctrl/cmd/main/main.go index 96bf01150..feb7766f4 100644 --- a/cyclops-ctrl/cmd/main/main.go +++ b/cyclops-ctrl/cmd/main/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/git" "os" "strconv" "time" @@ -86,8 +87,10 @@ func main() { panic(err) } + credsResolver := auth.NewTemplatesResolver(k8sClient) + templatesRepo := template.NewRepo( - auth.NewTemplatesResolver(k8sClient), + credsResolver, cache.NewInMemoryTemplatesCache(), ) @@ -101,8 +104,9 @@ func main() { prometheus.StartCacheMetricsUpdater(&monitor, templatesRepo.ReturnCache(), 10*time.Second, setupLog) helmReleaseClient := helm.NewReleaseClient(helmWatchNamespace) + gitWriteClient := git.NewWriteClient(credsResolver, getCommitMessageTemplate(), setupLog) - handler, err := handler.New(templatesRepo, k8sClient, helmReleaseClient, renderer, moduleTargetNamespace, telemetryClient, monitor) + handler, err := handler.New(templatesRepo, k8sClient, helmReleaseClient, renderer, gitWriteClient, moduleTargetNamespace, telemetryClient, monitor) if err != nil { panic(err) } @@ -192,3 +196,7 @@ func getHelmWatchNamespace() string { } return value } + +func getCommitMessageTemplate() string { + return os.Getenv("COMMIT_MESSAGE_TEMPLATE") +} diff --git a/cyclops-ctrl/internal/controller/modules.go b/cyclops-ctrl/internal/controller/modules.go index 7e9afe081..b2ac07c3a 100644 --- a/cyclops-ctrl/internal/controller/modules.go +++ b/cyclops-ctrl/internal/controller/modules.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/git" + "sigs.k8s.io/yaml" "github.com/gin-gonic/gin" @@ -27,6 +29,7 @@ type Modules struct { kubernetesClient k8sclient.IKubernetesClient templatesRepo template.ITemplateRepo renderer *render.Renderer + gitWriteClient *git.WriteClient moduleTargetNamespace string @@ -38,6 +41,7 @@ func NewModulesController( templatesRepo template.ITemplateRepo, kubernetes k8sclient.IKubernetesClient, renderer *render.Renderer, + gitWriteClient *git.WriteClient, moduleTargetNamespace string, telemetryClient telemetry.Client, monitor prometheus.Monitor, @@ -46,6 +50,7 @@ func NewModulesController( kubernetesClient: kubernetes, templatesRepo: templatesRepo, renderer: renderer, + gitWriteClient: gitWriteClient, moduleTargetNamespace: moduleTargetNamespace, telemetryClient: telemetryClient, monitor: monitor, @@ -126,6 +131,31 @@ func (m *Modules) ListModules(ctx *gin.Context) { func (m *Modules) DeleteModule(ctx *gin.Context) { ctx.Header("Access-Control-Allow-Origin", "*") + m.monitor.DecModule() + + deleteMethod := ctx.Query("deleteMethod") + + if deleteMethod == "git" { + module, err := m.kubernetesClient.GetModule(ctx.Param("name")) + if err != nil { + ctx.JSON(http.StatusBadRequest, dto.NewError("Error fetching module for deletion", err.Error())) + return + } + + if module == nil { + ctx.JSON(http.StatusBadRequest, dto.NewError("Error fetching module for deletion", "Check that the module exists")) + return + } + + err = m.gitWriteClient.DeleteModule(*module) + if err != nil { + ctx.JSON(http.StatusBadRequest, dto.NewError("Error deleting module from git", err.Error())) + return + } + + ctx.Status(http.StatusOK) + return + } err := m.kubernetesClient.DeleteModule(ctx.Param("name")) if err != nil { @@ -134,7 +164,6 @@ func (m *Modules) DeleteModule(ctx *gin.Context) { return } - m.monitor.DecModule() ctx.Status(http.StatusOK) } @@ -279,6 +308,15 @@ func (m *Modules) CreateModule(ctx *gin.Context) { m.telemetryClient.ModuleCreation() + if module.GetAnnotations() != nil && len(module.GetAnnotations()[v1alpha1.GitOpsWriteRepoAnnotation]) != 0 { + err := m.gitWriteClient.Write(module) + if err != nil { + fmt.Println(err) + ctx.JSON(http.StatusInternalServerError, dto.NewError("Error pushing to git", err.Error())) + } + return + } + err = m.kubernetesClient.CreateModule(module) if err != nil { fmt.Println(err) @@ -314,6 +352,57 @@ func (m *Modules) UpdateModule(ctx *gin.Context) { return } + module.Spec.TemplateRef.SourceType = curr.Spec.TemplateRef.SourceType + + module.Status.TemplateResolvedVersion = request.Template.ResolvedVersion + module.Status.ReconciliationStatus = curr.Status.ReconciliationStatus + module.Status.IconURL = curr.Status.IconURL + module.Status.ManagedGVRs = curr.Status.ManagedGVRs + + module.Spec.TargetNamespace = curr.Spec.TargetNamespace + module.SetLabels(curr.GetLabels()) + + annotations := curr.GetAnnotations() + moduleAnnotations := module.GetAnnotations() + + if annotations == nil { + annotations = make(map[string]string) + } + + if moduleAnnotations != nil { + if _, ok := moduleAnnotations["cyclops-ui.com/write-repo"]; ok { + annotations["cyclops-ui.com/write-repo"] = moduleAnnotations["cyclops-ui.com/write-repo"] + } + if _, ok := moduleAnnotations["cyclops-ui.com/write-path"]; ok { + annotations["cyclops-ui.com/write-path"] = moduleAnnotations["cyclops-ui.com/write-path"] + } + if _, ok := moduleAnnotations["cyclops-ui.com/write-revision"]; ok { + annotations["cyclops-ui.com/write-revision"] = moduleAnnotations["cyclops-ui.com/write-revision"] + } + } + + if len(moduleAnnotations) == 0 || len(moduleAnnotations[v1alpha1.GitOpsWriteRepoAnnotation]) == 0 { + delete(annotations, v1alpha1.GitOpsWriteRepoAnnotation) + } + if len(moduleAnnotations) == 0 || len(moduleAnnotations[v1alpha1.GitOpsWritePathAnnotation]) == 0 { + delete(annotations, v1alpha1.GitOpsWritePathAnnotation) + } + if len(moduleAnnotations) == 0 || len(moduleAnnotations[v1alpha1.GitOpsWriteRevisionAnnotation]) == 0 { + delete(annotations, v1alpha1.GitOpsWriteRevisionAnnotation) + } + + delete(annotations, "kubectl.kubernetes.io/last-applied-configuration") + module.SetAnnotations(annotations) + + if len(module.GetAnnotations()[v1alpha1.GitOpsWriteRepoAnnotation]) != 0 { + err := m.gitWriteClient.Write(module) + if err != nil { + fmt.Println(err) + ctx.JSON(http.StatusInternalServerError, dto.NewError("Error pushing to git", err.Error())) + } + return + } + history := curr.History if curr.History == nil { history = make([]v1alpha1.HistoryEntry, 0) @@ -336,16 +425,6 @@ func (m *Modules) UpdateModule(ctx *gin.Context) { module.SetResourceVersion(curr.GetResourceVersion()) - module.Spec.TemplateRef.SourceType = curr.Spec.TemplateRef.SourceType - - module.Status.TemplateResolvedVersion = request.Template.ResolvedVersion - module.Status.ReconciliationStatus = curr.Status.ReconciliationStatus - module.Status.IconURL = curr.Status.IconURL - module.Status.ManagedGVRs = curr.Status.ManagedGVRs - - module.Spec.TargetNamespace = curr.Spec.TargetNamespace - module.SetLabels(curr.GetLabels()) - result, err := m.kubernetesClient.UpdateModuleStatus(&module) if err != nil { fmt.Println(err) diff --git a/cyclops-ctrl/internal/git/writeclient.go b/cyclops-ctrl/internal/git/writeclient.go new file mode 100644 index 000000000..f0b3bc7bb --- /dev/null +++ b/cyclops-ctrl/internal/git/writeclient.go @@ -0,0 +1,257 @@ +package git + +import ( + "bytes" + "errors" + "fmt" + "github.com/go-logr/logr" + path2 "path" + "text/template" + "time" + + cyclopsv1alpha1 "github.com/cyclops-ui/cyclops/cyclops-ctrl/api/v1alpha1" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/auth" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "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/http" + "github.com/go-git/go-git/v5/storage/memory" + "sigs.k8s.io/yaml" +) + +type WriteClient struct { + templatesResolver auth.TemplatesResolver + commitMessageTemplate *template.Template +} + +const _defaultCommitMessageTemplate = "Update {{ .Name }} module config" + +func NewWriteClient(templatesResolver auth.TemplatesResolver, commitMessageTemplate string, logger logr.Logger) *WriteClient { + return &WriteClient{ + templatesResolver: templatesResolver, + commitMessageTemplate: getCommitMessageTemplate(commitMessageTemplate, logger), + } +} + +func getCommitMessageTemplate(commitMessageTemplate string, logger logr.Logger) *template.Template { + if commitMessageTemplate == "" { + return template.Must(template.New("commitMessage").Parse(_defaultCommitMessageTemplate)) + } + + tmpl, err := template.New("commitMessage").Parse(commitMessageTemplate) + if err != nil { + logger.Error(err, "failed to parse commit message template, falling back to the default commit message", "template", commitMessageTemplate) + return template.Must(template.New("commitMessage").Parse(_defaultCommitMessageTemplate)) + } + + return tmpl +} + +func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error { + module.Status.ReconciliationStatus = nil + module.Status.ManagedGVRs = nil + + repoURL, exists := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRepoAnnotation] + if !exists { + return errors.New(fmt.Sprintf("module passed to write without git repository; set cyclops-ui.com/write-repo annotation in module %v", module.Name)) + } + + path := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWritePathAnnotation] + revision := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRevisionAnnotation] + + creds, err := c.templatesResolver.RepoAuthCredentials(repoURL) + if err != nil { + return err + } + + storer := memory.NewStorage() + fs := memfs.New() + + repo, worktree, err := cloneRepo(repoURL, revision, storer, &fs, creds) + if err != nil { + if errors.Is(err, git.NoMatchingRefSpecError{}) { + storer = memory.NewStorage() + fs = memfs.New() + repo, worktree, err = cloneRepo(repoURL, "", storer, &fs, creds) + if err != nil { + return err + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(revision), + Create: true, + }) + if err != nil { + return err + } + } else { + return err + } + } + + path = moduleFilePath(path, module.Name) + + file, err := fs.Create(path) + if err != nil { + return fmt.Errorf("failed to create file in repository: %w", err) + } + + moduleData, err := yaml.Marshal(module) + if err != nil { + return err + } + + if _, err := file.Write(moduleData); err != nil { + return fmt.Errorf("failed to write JSON data to file: %w", err) + } + file.Close() + + if _, err := worktree.Add(path); err != nil { + fmt.Println("err worktree.Add", path) + return fmt.Errorf("failed to add file to worktree: %w", err) + } + + var o bytes.Buffer + err = c.commitMessageTemplate.Execute(&o, module.ObjectMeta) + if err != nil { + return err + } + + _, err = worktree.Commit(o.String(), &git.CommitOptions{ + Author: &object.Signature{ + Name: creds.Username, + When: time.Now(), + }, + }) + if err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + + if err := repo.Push(&git.PushOptions{ + Auth: httpBasicAuthCredentials(creds), + }); err != nil { + return fmt.Errorf("failed to push changes: %w", err) + } + + return nil +} + +func (c *WriteClient) DeleteModule(module cyclopsv1alpha1.Module) error { + repoURL, exists := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRepoAnnotation] + if !exists { + return errors.New(fmt.Sprintf("module passed to delete without git repository; set cyclops-ui.com/write-repo annotation in module %v", module.Name)) + } + + path := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWritePathAnnotation] + revision := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRevisionAnnotation] + + creds, err := c.templatesResolver.RepoAuthCredentials(repoURL) + if err != nil { + return err + } + + storer := memory.NewStorage() + fs := memfs.New() + + repo, worktree, err := cloneRepo(repoURL, revision, storer, &fs, creds) + if err != nil { + if errors.Is(err, git.NoMatchingRefSpecError{}) { + storer = memory.NewStorage() + fs = memfs.New() + repo, worktree, err = cloneRepo(repoURL, "", storer, &fs, creds) + if err != nil { + return err + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(revision), + Create: true, + }) + if err != nil { + return err + } + } else { + return err + } + } + + path = moduleFilePath(path, module.Name) + + err = fs.Remove(path) + if err != nil { + return fmt.Errorf("failed to remove file from repository: %w", err) + } + + if _, err := worktree.Add(path); err != nil { + return fmt.Errorf("failed to add changes to worktree: %w", err) + } + + var o bytes.Buffer + err = c.commitMessageTemplate.Execute(&o, module.ObjectMeta) + if err != nil { + return err + } + + _, err = worktree.Commit(o.String(), &git.CommitOptions{ + Author: &object.Signature{ + Name: creds.Username, + When: time.Now(), + }, + }) + if err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + + if err := repo.Push(&git.PushOptions{ + Auth: httpBasicAuthCredentials(creds), + }); err != nil { + return fmt.Errorf("failed to push changes: %w", err) + } + + return nil +} + +func moduleFilePath(path, moduleName string) string { + if path2.Ext(path) != "yaml" || path2.Ext(path) != "yml" { + path = path2.Join(path, fmt.Sprintf("%v.yaml", moduleName)) + } + + return path +} + +func cloneRepo(url, revision string, storer *memory.Storage, fs *billy.Filesystem, creds *auth.Credentials) (*git.Repository, *git.Worktree, error) { + cloneOpts := git.CloneOptions{ + URL: url, + Auth: httpBasicAuthCredentials(creds), + SingleBranch: true, + } + + if len(revision) != 0 { + cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(revision) + } + + repo, err := git.Clone(storer, *fs, &cloneOpts) + if err != nil { + return nil, nil, err + } + + worktree, err := repo.Worktree() + if err != nil { + return nil, nil, err + } + + return repo, worktree, nil +} + +func httpBasicAuthCredentials(creds *auth.Credentials) *http.BasicAuth { + if creds == nil { + return nil + } + + return &http.BasicAuth{ + Username: creds.Username, + Password: creds.Password, + } +} diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go index 0dca15f48..a8a295e33 100644 --- a/cyclops-ctrl/internal/handler/handler.go +++ b/cyclops-ctrl/internal/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/controller/sse" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/git" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/integrations/helm" "github.com/gin-gonic/gin" "net/http" @@ -17,10 +18,11 @@ import ( type Handler struct { router *gin.Engine - templatesRepo templaterepo.ITemplateRepo - k8sClient k8sclient.IKubernetesClient - releaseClient *helm.ReleaseClient - renderer *render.Renderer + templatesRepo templaterepo.ITemplateRepo + k8sClient k8sclient.IKubernetesClient + releaseClient *helm.ReleaseClient + renderer *render.Renderer + gitWriteClient *git.WriteClient moduleTargetNamespace string @@ -33,6 +35,7 @@ func New( kubernetesClient k8sclient.IKubernetesClient, releaseClient *helm.ReleaseClient, renderer *render.Renderer, + gitWriteClient *git.WriteClient, moduleTargetNamespace string, telemetryClient telemetry.Client, monitor prometheus.Monitor, @@ -42,6 +45,7 @@ func New( k8sClient: kubernetesClient, renderer: renderer, releaseClient: releaseClient, + gitWriteClient: gitWriteClient, moduleTargetNamespace: moduleTargetNamespace, telemetryClient: telemetryClient, monitor: monitor, @@ -52,7 +56,7 @@ func (h *Handler) Start() error { gin.SetMode(gin.DebugMode) templatesController := controller.NewTemplatesController(h.templatesRepo, h.k8sClient, h.telemetryClient) - modulesController := controller.NewModulesController(h.templatesRepo, h.k8sClient, h.renderer, h.moduleTargetNamespace, h.telemetryClient, h.monitor) + modulesController := controller.NewModulesController(h.templatesRepo, h.k8sClient, h.renderer, h.gitWriteClient, h.moduleTargetNamespace, h.telemetryClient, h.monitor) clusterController := controller.NewClusterController(h.k8sClient) helmController := controller.NewHelmController(h.k8sClient, h.releaseClient, h.telemetryClient) diff --git a/cyclops-ctrl/internal/mapper/modules.go b/cyclops-ctrl/internal/mapper/modules.go index 6a42456d0..6ffc746dc 100644 --- a/cyclops-ctrl/internal/mapper/modules.go +++ b/cyclops-ctrl/internal/mapper/modules.go @@ -20,13 +20,21 @@ func RequestToModule(req dto.Module) (cyclopsv1alpha1.Module, error) { return cyclopsv1alpha1.Module{}, err } + annotations := make(map[string]string) + if req.GitOpsWrite != nil && len(req.GitOpsWrite.Repo) != 0 { + annotations[cyclopsv1alpha1.GitOpsWriteRepoAnnotation] = req.GitOpsWrite.Repo + annotations[cyclopsv1alpha1.GitOpsWritePathAnnotation] = req.GitOpsWrite.Path + annotations[cyclopsv1alpha1.GitOpsWriteRevisionAnnotation] = req.GitOpsWrite.Branch + } + return cyclopsv1alpha1.Module{ TypeMeta: metav1.TypeMeta{ Kind: "Module", APIVersion: "cyclops-ui.com/v1alpha1", }, ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, + Name: req.Name, + Annotations: annotations, }, Spec: cyclopsv1alpha1.ModuleSpec{ TargetNamespace: mapTargetNamespace(req.Namespace), @@ -41,21 +49,30 @@ func RequestToModule(req dto.Module) (cyclopsv1alpha1.Module, error) { func ModuleToDTO(module cyclopsv1alpha1.Module) (dto.Module, error) { return dto.Module{ - Name: module.Name, - Namespace: module.Namespace, - TargetNamespace: mapTargetNamespace(module.Spec.TargetNamespace), - Version: module.Spec.TemplateRef.Version, - Template: k8sTemplateRefToDTO(module.Spec.TemplateRef, module.Status.TemplateResolvedVersion), - Values: module.Spec.Values, - IconURL: module.Status.IconURL, - ReconciliationStatus: dto.ReconciliationStatus{ - Status: dto.ReconciliationStatusState(module.Status.ReconciliationStatus.Status), - Reason: module.Status.ReconciliationStatus.Reason, - Errors: module.Status.ReconciliationStatus.Errors, - }, + Name: module.Name, + Namespace: module.Namespace, + TargetNamespace: mapTargetNamespace(module.Spec.TargetNamespace), + Version: module.Spec.TemplateRef.Version, + Template: k8sTemplateRefToDTO(module.Spec.TemplateRef, module.Status.TemplateResolvedVersion), + Values: module.Spec.Values, + IconURL: module.Status.IconURL, + GitOpsWrite: mapGitOpsWrite(module), + ReconciliationStatus: ReconciliationStatusToDTO(module.Status.ReconciliationStatus), }, nil } +func ReconciliationStatusToDTO(status *cyclopsv1alpha1.ReconciliationStatus) dto.ReconciliationStatus { + if status == nil { + return dto.ReconciliationStatus{Status: dto.Unknown} + } + + return dto.ReconciliationStatus{ + Status: dto.ReconciliationStatusState(status.Status), + Reason: status.Reason, + Errors: status.Errors, + } +} + func ModuleListToDTO(modules []cyclopsv1alpha1.Module) []dto.Module { out := make([]dto.Module, 0, len(modules)) @@ -152,3 +169,15 @@ func mapTargetNamespace(targetNamespace string) string { return targetNamespace } + +func mapGitOpsWrite(module cyclopsv1alpha1.Module) *dto.GitOpsWrite { + if repo, ok := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRepoAnnotation]; !ok || len(repo) == 0 { + return nil + } + + return &dto.GitOpsWrite{ + Repo: module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRepoAnnotation], + Path: module.GetAnnotations()[cyclopsv1alpha1.GitOpsWritePathAnnotation], + Branch: module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRevisionAnnotation], + } +} diff --git a/cyclops-ctrl/internal/models/dto/modules.go b/cyclops-ctrl/internal/models/dto/modules.go index c1e4339d3..9eaf8ff20 100644 --- a/cyclops-ctrl/internal/models/dto/modules.go +++ b/cyclops-ctrl/internal/models/dto/modules.go @@ -4,6 +4,7 @@ type Module struct { Name string `json:"name"` Namespace string `json:"namespace"` TargetNamespace string `json:"targetNamespace"` + GitOpsWrite *GitOpsWrite `json:"gitOpsWrite,omitempty"` Template Template `json:"template"` Version string `json:"version"` Values interface{} `json:"values"` @@ -35,6 +36,12 @@ type Template struct { SourceType string `json:"sourceType"` } +type GitOpsWrite struct { + Repo string `json:"repo"` + Path string `json:"path"` + Branch string `json:"branch"` +} + type TemplatesResponse struct { Current string `json:"current"` New string `json:"new"` diff --git a/cyclops-ctrl/internal/modulecontroller/module_controller.go b/cyclops-ctrl/internal/modulecontroller/module_controller.go index 533efcdd5..190bcca1a 100644 --- a/cyclops-ctrl/internal/modulecontroller/module_controller.go +++ b/cyclops-ctrl/internal/modulecontroller/module_controller.go @@ -443,7 +443,7 @@ func (r *ModuleReconciler) setStatus( } module.Status = cyclopsv1alpha1.ModuleStatus{ - ReconciliationStatus: cyclopsv1alpha1.ReconciliationStatus{ + ReconciliationStatus: &cyclopsv1alpha1.ReconciliationStatus{ Status: status, Reason: reason, Errors: installErrors, diff --git a/cyclops-ui/src/components/pages/ModuleDetails/custom.css b/cyclops-ui/src/components/pages/ModuleDetails/custom.css deleted file mode 100644 index 20632cf6d..000000000 --- a/cyclops-ui/src/components/pages/ModuleDetails/custom.css +++ /dev/null @@ -1,12 +0,0 @@ -.ant-col.ant-form-item-label { - padding: 0 !important; -} - -label.ant-form-item-required:before { - bottom: 1em !important; - position: relative !important; -} - -code { - color: #000080; -} diff --git a/cyclops-ui/src/components/shared/CreateModule/CreateModule.tsx b/cyclops-ui/src/components/shared/CreateModule/CreateModule.tsx index 011d70205..a6fb139d7 100644 --- a/cyclops-ui/src/components/shared/CreateModule/CreateModule.tsx +++ b/cyclops-ui/src/components/shared/CreateModule/CreateModule.tsx @@ -14,6 +14,7 @@ import { notification, theme, ConfigProvider, + Switch, } from "antd"; import { deepMerge, @@ -74,6 +75,7 @@ export interface CreateModuleProps { moduleNamespace: string, templateRef: any, values: string, + gitOpsWrite?: any, ) => Promise; onSubmitModuleSuccess: (moduleName: string) => void; onBackButton: () => void; @@ -127,6 +129,8 @@ export const CreateModuleComponent = ({ const [namespaces, setNamespaces] = useState([]); + const [gitopsToggle, SetGitopsToggle] = useState(false); + const [notificationApi, contextHolder] = notification.useNotification(); const openNotification = (errors: FeedbackError[]) => { notificationApi.error({ @@ -167,6 +171,13 @@ export const CreateModuleComponent = ({ const handleSubmit = (values: any) => { const moduleName = values["cyclops_module_name"]; const moduleNamespace = values["cyclops_module_namespace"]; + const gitOpsWrite = gitopsToggle + ? { + repo: values["gitops-repo"], + path: values["gitops-path"], + branch: values["gitops-branch"], + } + : null; values = findMaps(config.root.properties, values, initialValuesRaw); @@ -180,6 +191,7 @@ export const CreateModuleComponent = ({ sourceType: template.sourceType, }, values, + gitOpsWrite, ) .then(() => { onSubmitModuleSuccess(moduleName); @@ -568,6 +580,76 @@ export const CreateModuleComponent = ({ +
+ + + Push changes to Git? +

+ Instead of deploying to the cluster, Cyclops will + push the changes to a git repository. +

+
+ } + style={{ padding: "0px 12px 0px 12px" }} + > + { + SetGitopsToggle(e); + }} + /> + +
+ + + + + + + + + + + +
+
Promise; onUpdateModuleSuccess: (moduleName: string) => void; onBackButton: (moduleName: string) => void; @@ -90,12 +97,14 @@ export const EditModuleComponent = ({ const [form] = Form.useForm(); const [editTemplateForm] = Form.useForm(); + const [gitOpsWriteForm] = Form.useForm(); const [initialValuesRaw, setInitialValuesRaw] = useState({}); const [values, setValues] = useState({}); const [isChanged, setIsChanged] = useState(false); const [isTemplateChanged, setIsTemplateChanged] = useState(false); + const [isGitOpsChanged, setIsGitOpsChanged] = useState(false); const [config, setConfig] = useState