diff --git a/cyclops-ctrl/api/v1alpha1/module_types.go b/cyclops-ctrl/api/v1alpha1/module_types.go index e8f25815..a56b3642 100644 --- a/cyclops-ctrl/api/v1alpha1/module_types.go +++ b/cyclops-ctrl/api/v1alpha1/module_types.go @@ -54,6 +54,12 @@ const ( MCPServerModuleLabel = "cyclops-ui.com/mcp-server" ) +type GitOpsWriteDestination struct { + Repo string `json:"repo"` + Path string `json:"path"` + Version string `json:"version"` +} + type TemplateRef struct { URL string `json:"repo"` Path string `json:"path"` @@ -62,6 +68,9 @@ type TemplateRef struct { // +kubebuilder:validation:Enum=git;helm;oci // +kubebuilder:validation:Optional SourceType TemplateSourceType `json:"sourceType,omitempty"` + + // +kubebuilder:validation:Optional + EnforceGitOpsWrite *GitOpsWriteDestination `json:"enforceGitOpsWrite,omitempty"` } type TemplateGitRef struct { diff --git a/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go b/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go index 633221cd..b1d4e99a 100644 --- a/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go +++ b/cyclops-ctrl/api/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitOpsWriteDestination) DeepCopyInto(out *GitOpsWriteDestination) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitOpsWriteDestination. +func (in *GitOpsWriteDestination) DeepCopy() *GitOpsWriteDestination { + if in == nil { + return nil + } + out := new(GitOpsWriteDestination) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupVersionResource) DeepCopyInto(out *GroupVersionResource) { *out = *in @@ -140,7 +155,7 @@ func (in *ModuleList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ModuleSpec) DeepCopyInto(out *ModuleSpec) { *out = *in - out.TemplateRef = in.TemplateRef + in.TemplateRef.DeepCopyInto(&out.TemplateRef) in.Values.DeepCopyInto(&out.Values) } @@ -307,6 +322,11 @@ func (in *TemplateGitRef) DeepCopy() *TemplateGitRef { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateRef) DeepCopyInto(out *TemplateRef) { *out = *in + if in.EnforceGitOpsWrite != nil { + in, out := &in.EnforceGitOpsWrite, &out.EnforceGitOpsWrite + *out = new(GitOpsWriteDestination) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateRef. @@ -324,7 +344,7 @@ func (in *TemplateStore) DeepCopyInto(out *TemplateStore) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateStore. diff --git a/cyclops-ctrl/config/crd/bases/cyclops-ui.com_modules.yaml b/cyclops-ctrl/config/crd/bases/cyclops-ui.com_modules.yaml index f840e80d..13b59d88 100644 --- a/cyclops-ctrl/config/crd/bases/cyclops-ui.com_modules.yaml +++ b/cyclops-ctrl/config/crd/bases/cyclops-ui.com_modules.yaml @@ -104,6 +104,19 @@ spec: type: string template: properties: + enforceGitOpsWrite: + properties: + path: + type: string + repo: + type: string + version: + type: string + required: + - path + - repo + - version + type: object path: type: string repo: diff --git a/cyclops-ctrl/config/crd/bases/cyclops-ui.com_templatestores.yaml b/cyclops-ctrl/config/crd/bases/cyclops-ui.com_templatestores.yaml index dc0a5cac..9a856ec0 100644 --- a/cyclops-ctrl/config/crd/bases/cyclops-ui.com_templatestores.yaml +++ b/cyclops-ctrl/config/crd/bases/cyclops-ui.com_templatestores.yaml @@ -55,6 +55,19 @@ spec: type: object spec: properties: + enforceGitOpsWrite: + properties: + path: + type: string + repo: + type: string + version: + type: string + required: + - path + - repo + - version + type: object path: type: string repo: diff --git a/cyclops-ctrl/internal/git/writeclient.go b/cyclops-ctrl/internal/git/writeclient.go index a4e9abfc..63a493a1 100644 --- a/cyclops-ctrl/internal/git/writeclient.go +++ b/cyclops-ctrl/internal/git/writeclient.go @@ -4,14 +4,13 @@ import ( "bytes" "errors" "fmt" + json "github.com/json-iterator/go" path2 "path" "text/template" "time" - "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/auth" "github.com/go-logr/logr" - cyclopsv1alpha1 "github.com/cyclops-ui/cyclops/cyclops-ctrl/api/v1alpha1" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" @@ -20,6 +19,9 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" "sigs.k8s.io/yaml" + + cyclopsv1alpha1 "github.com/cyclops-ui/cyclops/cyclops-ctrl/api/v1alpha1" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/auth" ) type WriteClient struct { @@ -50,6 +52,32 @@ func getCommitMessageTemplate(commitMessageTemplate string, logger logr.Logger) return tmpl } +func getModulePath(module cyclopsv1alpha1.Module) (string, error) { + path := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWritePathAnnotation] + + tmpl, err := template.New("modulePath").Parse(path) + if err != nil { + return "", err + } + + moduleMap := make(map[string]interface{}) + moduleData, err := json.Marshal(module) + if err != nil { + return "", err + } + if err := json.Unmarshal(moduleData, &moduleMap); err != nil { + return "", err + } + + var o bytes.Buffer + err = tmpl.Execute(&o, moduleMap) + if err != nil { + return "", err + } + + return o.String(), nil +} + func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error { module.Status.ReconciliationStatus = nil module.Status.ManagedGVRs = nil @@ -59,7 +87,11 @@ func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error { 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] + path, err := getModulePath(module) + if err != nil { + return err + } + revision := module.GetAnnotations()[cyclopsv1alpha1.GitOpsWriteRevisionAnnotation] creds, err := c.templatesResolver.RepoAuthCredentials(repoURL) @@ -67,6 +99,10 @@ func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error { return err } + if creds == nil { + return errors.New(fmt.Sprintf("failed to fetch creds for repo %v: check template auth rules", repoURL)) + } + storer := memory.NewStorage() fs := memfs.New() diff --git a/cyclops-ctrl/internal/mapper/templatestore.go b/cyclops-ctrl/internal/mapper/templatestore.go index 0fc57ac7..3f1e3f2b 100644 --- a/cyclops-ctrl/internal/mapper/templatestore.go +++ b/cyclops-ctrl/internal/mapper/templatestore.go @@ -16,6 +16,15 @@ func TemplateStoreListToDTO(store []v1alpha1.TemplateStore) []dto.TemplateStore iconURL = templateStore.GetAnnotations()[v1alpha1.IconURLAnnotation] } + var enforceGitOpsWrite *dto.GitOpsWrite + if templateStore.Spec.EnforceGitOpsWrite != nil { + enforceGitOpsWrite = &dto.GitOpsWrite{ + Repo: templateStore.Spec.EnforceGitOpsWrite.Repo, + Path: templateStore.Spec.EnforceGitOpsWrite.Path, + Branch: templateStore.Spec.EnforceGitOpsWrite.Version, + } + } + out = append(out, dto.TemplateStore{ Name: templateStore.Name, IconURL: iconURL, @@ -25,6 +34,7 @@ func TemplateStoreListToDTO(store []v1alpha1.TemplateStore) []dto.TemplateStore Version: templateStore.Spec.Version, SourceType: string(templateStore.Spec.SourceType), }, + EnforceGitOpsWrite: enforceGitOpsWrite, }) } @@ -32,6 +42,15 @@ func TemplateStoreListToDTO(store []v1alpha1.TemplateStore) []dto.TemplateStore } func DTOToTemplateStore(store dto.TemplateStore, iconURL string) *v1alpha1.TemplateStore { + var enforceGitOpsWrite *v1alpha1.GitOpsWriteDestination + if store.EnforceGitOpsWrite != nil { + enforceGitOpsWrite = &v1alpha1.GitOpsWriteDestination{ + Repo: store.EnforceGitOpsWrite.Repo, + Path: store.EnforceGitOpsWrite.Path, + Version: store.EnforceGitOpsWrite.Branch, + } + } + return &v1alpha1.TemplateStore{ TypeMeta: metav1.TypeMeta{ Kind: "TemplateStore", @@ -44,10 +63,11 @@ func DTOToTemplateStore(store dto.TemplateStore, iconURL string) *v1alpha1.Templ }, }, Spec: v1alpha1.TemplateRef{ - URL: store.TemplateRef.URL, - Path: store.TemplateRef.Path, - Version: store.TemplateRef.Version, - SourceType: v1alpha1.TemplateSourceType(store.TemplateRef.SourceType), + URL: store.TemplateRef.URL, + Path: store.TemplateRef.Path, + Version: store.TemplateRef.Version, + SourceType: v1alpha1.TemplateSourceType(store.TemplateRef.SourceType), + EnforceGitOpsWrite: enforceGitOpsWrite, }, } } diff --git a/cyclops-ctrl/internal/models/dto/templatestore.go b/cyclops-ctrl/internal/models/dto/templatestore.go index 9b59ed1a..e705efb1 100644 --- a/cyclops-ctrl/internal/models/dto/templatestore.go +++ b/cyclops-ctrl/internal/models/dto/templatestore.go @@ -1,7 +1,8 @@ package dto type TemplateStore struct { - Name string `json:"name" binding:"required"` - IconURL string `json:"iconURL"` - TemplateRef Template `json:"ref"` + Name string `json:"name" binding:"required"` + IconURL string `json:"iconURL"` + TemplateRef Template `json:"ref"` + EnforceGitOpsWrite *GitOpsWrite `json:"enforceGitOpsWrite,omitempty"` } diff --git a/cyclops-ctrl/internal/modulecontroller/module_controller.go b/cyclops-ctrl/internal/modulecontroller/module_controller.go index 2b28e1c4..d0bc6679 100644 --- a/cyclops-ctrl/internal/modulecontroller/module_controller.go +++ b/cyclops-ctrl/internal/modulecontroller/module_controller.go @@ -87,13 +87,6 @@ func NewModuleReconciler( // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Module object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile func (r *ModuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) r.telemetryClient.ModuleReconciliation() diff --git a/cyclops-ui/src/components/pages/Modules/Modules.tsx b/cyclops-ui/src/components/pages/Modules/Modules.tsx index 3521a09f..c4aff753 100644 --- a/cyclops-ui/src/components/pages/Modules/Modules.tsx +++ b/cyclops-ui/src/components/pages/Modules/Modules.tsx @@ -107,6 +107,7 @@ const Modules = () => { const handleClick = () => { window.location.href = "/modules/new"; }; + const handleSelectItem = (selectedItems: any[]) => { setModuleHealthFilter(selectedItems); }; diff --git a/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx b/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx index 15fb1e60..392328f6 100644 --- a/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx +++ b/cyclops-ui/src/components/pages/TemplateStore/TemplateStore.tsx @@ -15,9 +15,9 @@ import { Radio, Popover, Checkbox, + Switch, } from "antd"; import axios from "axios"; -import Title from "antd/es/typography/Title"; import { DeleteOutlined, EditOutlined, @@ -36,8 +36,12 @@ import { import gitLogo from "../../../static/img/git.png"; import helmLogo from "../../../static/img/helm.png"; import dockerLogo from "../../../static/img/docker-mark-blue.png"; +import { useTheme } from "../../theme/ThemeContext"; +import Title from "antd/es/typography/Title"; const TemplateStore = () => { + const { mode } = useTheme(); + const [templates, setTemplates] = useState([]); const [query, setQuery] = useState(""); const [filteredTemplates, setFilteredTemplates] = useState([]); @@ -52,6 +56,7 @@ const TemplateStore = () => { const [requestStatus, setRequestStatus] = useState<{ [key: string]: string }>( {}, ); + const [enforceGitOpsEnabled, setEnforceGitOpsEnabled] = useState(false); const [error, setError] = useState({ message: "", description: "", @@ -274,6 +279,73 @@ const TemplateStore = () => { ); }; + const advancedTemplateGitOpsWrite = () => { + return ( +
+ Configure GitOps settings to push changes to a git repository + instead of deploying directly to the cluster. +
+