Skip to content

Commit

Permalink
delete module method (#721)
Browse files Browse the repository at this point in the history
* select module delete method

* delete modules through git write client

* call delete endpoint with delete method query

* remove unused import

* remove layout from form item
  • Loading branch information
petar-cvit authored Jan 10, 2025
1 parent 7cad264 commit 055e313
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 15 deletions.
26 changes: 25 additions & 1 deletion cyclops-ctrl/internal/controller/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,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 {
Expand All @@ -139,7 +164,6 @@ func (m *Modules) DeleteModule(ctx *gin.Context) {
return
}

m.monitor.DecModule()
ctx.Status(http.StatusOK)
}

Expand Down
82 changes: 79 additions & 3 deletions cyclops-ctrl/internal/git/writeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error {
}
}

if path2.Ext(path) != "yaml" || path2.Ext(path) != "yml" {
path = path2.Join(path, fmt.Sprintf("%v.yaml", module.Name))
}
path = moduleFilePath(path, module.Name)

file, err := fs.Create(path)
if err != nil {
Expand Down Expand Up @@ -114,6 +112,84 @@ func (c *WriteClient) Write(module cyclopsv1alpha1.Module) error {
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)
}

commitMessage := fmt.Sprintf("delete module on path %s", path)
_, err = worktree.Commit(commitMessage, &git.CommitOptions{
Author: &object.Signature{
Name: "Cyclops UI",
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
ConfigProvider,
Descriptions,
Divider,
Form,
Input,
Modal,
notification,
Popover,
Radio,
Row,
Spin,
Tooltip,
Expand Down Expand Up @@ -95,6 +98,11 @@ interface module {
targetNamespace: string;
template: templateRef;
iconURL: string;
gitOpsWrite: {
repo: string;
path: string;
branch: string;
};
}

export interface ModuleResourceDetailsProps {
Expand All @@ -105,7 +113,7 @@ export interface ModuleResourceDetailsProps {
fetchModuleRawManifest: (moduleName: string) => Promise<string>;
fetchModuleRenderedManifest: (moduleName: string) => Promise<string>;
reconcileModule: (moduleName: string) => Promise<any>;
deleteModule: (moduleName: string) => Promise<any>;
deleteModule: (moduleName: string, deleteMethod?: string) => Promise<any>;
onDeleteModuleSuccess: (moduleName: string) => void;
fetchModuleResources: (moduleName: string) => Promise<any[]>;
fetchResource: (
Expand Down Expand Up @@ -191,6 +199,7 @@ export const ModuleResourceDetails = ({
const [loadingReconciliation, setLoadingReconciliation] = useState(false);

const [deleteName, setDeleteName] = useState("");
const [deleteModuleForm] = Form.useForm();

const [resources, setResources] = useState<any[]>([]);
const [workloads, setWorkloads] = useState<Map<string, Workload>>(new Map());
Expand Down Expand Up @@ -223,6 +232,11 @@ export const ModuleResourceDetails = ({
sourceType: "",
},
iconURL: "",
gitOpsWrite: {
repo: "",
path: "",
branch: "",
},
});

const [loadingRawManifest, setLoadingRawManifest] = useState(false);
Expand Down Expand Up @@ -298,8 +312,8 @@ export const ModuleResourceDetails = ({
setLoading(false);
};

const deleteDeployment = () => {
deleteModule(name)
const handleDeleteModule = (deleteModuleValues) => {
deleteModule(name, deleteModuleValues.method)
.then(() => {
onDeleteModuleSuccess(name);
})
Expand Down Expand Up @@ -327,6 +341,87 @@ export const ModuleResourceDetails = ({
return resourcesToDelete;
};

const isModuleSourceGit = () => {
return (
module.gitOpsWrite &&
typeof module.gitOpsWrite.repo === "string" &&
module.gitOpsWrite.repo.trim() !== ""
);
};

const selectDeletionMethod = () => {
if (!isModuleSourceGit()) {
return <></>;
}

return (
<Form.Item
name="method"
style={{ width: "100%", marginBottom: "12px" }}
rules={[
{
required: true,
message: "Select module deletion method!",
},
]}
label={
<div>
Deletion Method
<p style={{ color: "#8b8e91", marginBottom: "0px" }}>
Select how you want to delete this Module.
</p>
</div>
}
>
<Radio.Group
optionType="button"
style={{
width: "100%",
display: "flex",
}}
>
<Popover
content={
"Deletes the Module from the cluster directly and doesnt remove the file from Git"
}
placement={"topRight"}
>
<Radio.Button
value="cluster"
style={{
flex: 1,
textAlign: "center",
}}
>
In-cluster
</Radio.Button>
</Popover>
<Popover
content={
<div>
<Row>Deletes the Module config file from the git repo:</Row>
<Row style={{ color: "#777" }}>
{module.gitOpsWrite.repo} / {module.gitOpsWrite.path}
</Row>
</div>
}
placement={"topLeft"}
>
<Radio.Button
value="git"
style={{
flex: 1,
textAlign: "center",
}}
>
Git repo
</Radio.Button>
</Popover>
</Radio.Group>
</Form.Item>
);
};

const moduleLoading = () => {
if (!loadModule) {
return <Spin />;
Expand Down Expand Up @@ -749,7 +844,10 @@ export const ModuleResourceDetails = ({
danger
block
disabled={deleteName !== name}
onClick={deleteDeployment}
onClick={() => {
console.log("delte button hamndler");
deleteModuleForm.submit();
}}
>
Delete
</Button>
Expand All @@ -762,11 +860,23 @@ export const ModuleResourceDetails = ({
>
Child resources
</Divider>
{getResourcesToDelete()}
<Divider style={{ fontSize: "120%" }} orientationMargin="0" />
In order to delete this module and related resources, type the name of
the module in the box below
<Input placeholder={name} required onChange={changeDeleteName} />
<Form
form={deleteModuleForm}
onFinish={handleDeleteModule}
layout={"vertical"}
>
{getResourcesToDelete()}
<Divider style={{ fontSize: "120%" }} orientationMargin="0" />
{selectDeletionMethod()}
<Form.Item
name="deleteModuleName"
label={
"In order to delete this module and related resources, type the name of the module in the box below"
}
>
<Input placeholder={name} required onChange={changeDeleteName} />
</Form.Item>
</Form>
</Modal>
<Modal
title="Module manifest"
Expand Down
8 changes: 6 additions & 2 deletions cyclops-ui/src/utils/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ export async function reconcileModule(moduleName: string) {
return await axios.post(`/api/modules/${moduleName}/reconcile`);
}

export async function deleteModule(moduleName: string) {
return await axios.delete(`/api/modules/${moduleName}`);
export async function deleteModule(moduleName: string, deleteMethod?: string) {
return await axios.delete(`/api/modules/${moduleName}`, {
params: {
deleteMethod: deleteMethod,
},
});
}

export async function fetchModuleResources(moduleName: string) {
Expand Down

0 comments on commit 055e313

Please sign in to comment.