From b26e6097195e1af99a42236657a6aeca2509ebe2 Mon Sep 17 00:00:00 2001 From: SquirrelDevelopper Date: Tue, 5 Nov 2024 16:46:06 +0100 Subject: [PATCH] Support for Docker stacks git repository This also commit introduces alert event handling in the real-time notification system. Additionally, it renames and refactors several enums and components relating to repositories, aligning with the new type nomenclature. Both the `Git` and `Local` repository types have been updated, alongside improvements in the custom stacks and playbooks repository controllers. --- .../workflows/pr-to-release-branch-on-tag.yml | 31 ++ client/package-lock.json | 30 +- client/package.json | 8 +- client/src/app.tsx | 2 + .../components/Alert/AlertNotification.tsx | 35 +++ client/src/components/Icons/CustomIcons.tsx | 19 ++ .../NewUnManagedDeviceModal.tsx | 2 + client/src/pages/Admin/Inventory/index.tsx | 5 + client/src/pages/Admin/Settings/Settings.tsx | 10 + .../components/ContainerStacksSettings.tsx | 166 ++++++++++ .../Settings/components/PlaybooksSettings.tsx | 61 +++- .../ContainerStacksGitRepositoryModal.tsx | 205 +++++++++++++ ...al.tsx => PlaybooksGitRepositoryModal.tsx} | 153 ++++----- ....tsx => PlaybooksLocalRepositoryModal.tsx} | 55 ++-- .../{ => forms}/DirectoryExclusionForm.tsx | 0 .../subcomponents/forms/FileMatchesForm.tsx | 52 ++++ .../subcomponents/forms/GitForm.tsx | 73 +++++ client/src/pages/ComposeEditor/index.tsx | 4 +- .../components/PlaybookDropdownMenu.tsx | 24 +- .../Playbooks/components/TreeComponent.tsx | 6 +- .../rest/container-stacks-repositories.ts | 151 +++++++++ .../services/rest/playbooks-repositories.ts | 54 ++-- docker-compose.dev.yml | 3 +- docker-compose.prod.yml | 3 +- docker-compose.yml | 3 +- server/package-lock.json | 148 +++++---- server/package.json | 8 +- server/src/config.ts | 2 + .../rest/containers-stacks-repository/git.ts | 154 ++++++++++ .../git.validator.ts | 30 ++ .../src/controllers/rest/containers/stacks.ts | 11 +- .../rest/playbooks-repository/git.ts | 18 +- .../rest/playbooks-repository/local.ts | 12 +- .../src/controllers/rest/settings/advanced.ts | 2 +- server/src/controllers/rest/user/user.ts | 2 +- server/src/core/events/events.ts | 1 + server/src/core/startup/index.ts | 13 +- .../database/model/ContainerCustomStack.ts | 25 +- .../model/ContainerCustomStackRepository.ts | 81 +++++ .../database/model/PlaybooksRepository.ts | 14 +- .../repository/ContainerCustomStackRepo.ts | 17 + .../ContainerCustomStackRepositoryRepo.ts | 41 +++ .../repository/PlaybooksRepositoryRepo.ts | 6 +- .../ansible/AnsibleConfigurationHelper.ts | 5 +- server/src/helpers/files/recursive-find.ts | 41 +++ server/src/helpers/git/CREDIT.md | 21 ++ server/src/helpers/git/commitAndSync.ts | 7 +- server/src/helpers/git/errors.ts | 4 +- server/src/helpers/git/init.ts | 4 +- server/src/helpers/git/initGit.ts | 6 +- server/src/helpers/git/inspect.ts | 35 +-- server/src/helpers/git/sync.ts | 4 +- server/src/modules/real-time/RealTime.ts | 17 +- ...ontainerCustomStacksRepositoryComponent.ts | 290 ++++++++++++++++++ .../ContainerCustomStacksRepositoryEngine.ts | 115 +++++++ .../PlaybooksRepositoryComponent.ts | 7 +- .../PlaybooksRepositoryEngine.ts | 16 +- .../default-playbooks-repositories.ts} | 15 +- .../GitPlaybooksRepositoryComponent.ts} | 52 +++- .../LocalPlaybooksRepositoryComponent.ts} | 11 +- .../tree-utils.ts | 0 .../managers/AnsibleShellCommandsManager.ts | 7 +- .../shell/managers/FileSystemManager.ts | 4 + .../src/routes/container-stacks-repository.ts | 47 +++ server/src/routes/index.ts | 2 + .../GitCustomStacksRepositoryUseCases.ts | 118 +++++++ ...s.ts => GitPlaybooksRepositoryUseCases.ts} | 37 ++- ...ts => LocalPlaybooksRepositoryUseCases.ts} | 10 +- .../services/PlaybooksRepositoryUseCases.ts | 6 +- .../AnsibleConfigurationHelper.test.ts | 2 +- .../modules/ansible/AnsibleCmd.test.ts | 2 +- .../PlaybookRepositoryComponent.test.ts | 11 +- .../playbooks-repository/tree-utils.test.ts | 2 +- shared-lib/src/enums/alert.ts | 3 + shared-lib/src/enums/playbooks.ts | 4 - shared-lib/src/enums/repositories.ts | 4 + shared-lib/src/index.ts | 3 +- shared-lib/src/types/api.ts | 21 +- shared-lib/src/types/events.ts | 4 + 79 files changed, 2276 insertions(+), 401 deletions(-) create mode 100644 .github/workflows/pr-to-release-branch-on-tag.yml create mode 100644 client/src/components/Alert/AlertNotification.tsx create mode 100644 client/src/pages/Admin/Settings/components/ContainerStacksSettings.tsx create mode 100644 client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx rename client/src/pages/Admin/Settings/components/subcomponents/{GitRepositoryModal.tsx => PlaybooksGitRepositoryModal.tsx} (52%) rename client/src/pages/Admin/Settings/components/subcomponents/{LocalRepositoryModal.tsx => PlaybooksLocalRepositoryModal.tsx} (77%) rename client/src/pages/Admin/Settings/components/subcomponents/{ => forms}/DirectoryExclusionForm.tsx (100%) create mode 100644 client/src/pages/Admin/Settings/components/subcomponents/forms/FileMatchesForm.tsx create mode 100644 client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx create mode 100644 client/src/services/rest/container-stacks-repositories.ts create mode 100644 server/src/controllers/rest/containers-stacks-repository/git.ts create mode 100644 server/src/controllers/rest/containers-stacks-repository/git.validator.ts create mode 100644 server/src/data/database/model/ContainerCustomStackRepository.ts create mode 100644 server/src/data/database/repository/ContainerCustomStackRepositoryRepo.ts create mode 100644 server/src/helpers/files/recursive-find.ts create mode 100644 server/src/helpers/git/CREDIT.md create mode 100644 server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts create mode 100644 server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts rename server/src/modules/{playbooks-repository => repository}/PlaybooksRepositoryComponent.ts (96%) rename server/src/modules/{playbooks-repository => repository}/PlaybooksRepositoryEngine.ts (87%) rename server/src/modules/{playbooks-repository/default-repositories.ts => repository/default-playbooks-repositories.ts} (73%) rename server/src/modules/{playbooks-repository/git-repository/GitRepositoryComponent.ts => repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts} (63%) rename server/src/modules/{playbooks-repository/local-repository/LocalRepositoryComponent.ts => repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent.ts} (59%) rename server/src/modules/{playbooks-repository => repository}/tree-utils.ts (100%) create mode 100644 server/src/routes/container-stacks-repository.ts create mode 100644 server/src/services/GitCustomStacksRepositoryUseCases.ts rename server/src/services/{GitRepositoryUseCases.ts => GitPlaybooksRepositoryUseCases.ts} (57%) rename server/src/services/{LocalRepositoryUseCases.ts => LocalPlaybooksRepositoryUseCases.ts} (82%) create mode 100644 shared-lib/src/enums/alert.ts delete mode 100644 shared-lib/src/enums/playbooks.ts create mode 100644 shared-lib/src/enums/repositories.ts diff --git a/.github/workflows/pr-to-release-branch-on-tag.yml b/.github/workflows/pr-to-release-branch-on-tag.yml new file mode 100644 index 00000000..76e02826 --- /dev/null +++ b/.github/workflows/pr-to-release-branch-on-tag.yml @@ -0,0 +1,31 @@ +name: Create PR to Release Branch + +on: + push: + tags: [ 'v*.*.*' ] + +jobs: + create-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Fetch all branches + run: git fetch --all + + - name: Create pull request + uses: peter-evans/create-pull-request@v5 + with: + source-branch: master + destination-branch: release + commit-message: 'chore: Merge master into release branch' + title: '[CHORE] Create PR from master to release' + body: 'The workflow is triggered by publishing a new tag. This PR merges changes from master to release.' + branch: 'pr-branch' \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index b1655dda..97376636 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -27,12 +27,12 @@ "classnames": "^2.5.1", "dayjs": "^1.11.13", "express": "^5.0.0", - "framer-motion": "^11.11.10", + "framer-motion": "^11.11.11", "lodash": "^4.17.21", "moment": "^2.30.1", "monaco-editor-webpack-plugin": "^7.1.0", "monaco-languageserver-types": "^0.4.0", - "monaco-yaml": "^5.2.2", + "monaco-yaml": "^5.2.3", "rc-banner-anim": "^2.4.5", "rc-menu": "^9.15.1", "rc-queue-anim": "^2.0.0", @@ -57,13 +57,13 @@ "@babel/preset-typescript": "^7.26.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.13.0", - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/classnames": "^2.3.4", "@types/express": "^5.0.0", "@types/history": "^5.0.0", "@types/jest": "^29.5.14", - "@types/lodash": "^4.17.12", + "@types/lodash": "^4.17.13", "@types/luxon": "^3.4.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -8460,9 +8460,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", - "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, "license": "MIT", "dependencies": { @@ -8821,9 +8821,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", - "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", "dev": true, "license": "MIT" }, @@ -26999,9 +26999,9 @@ } }, "node_modules/framer-motion": { - "version": "11.11.10", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz", - "integrity": "sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==", + "version": "11.11.11", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.11.tgz", + "integrity": "sha512-tuDH23ptJAKUHGydJQII9PhABNJBpB+z0P1bmgKK9QFIssHGlfPd6kxMq00LSKwE27WFsb2z0ovY0bpUyMvfRw==", "license": "MIT", "dependencies": { "tslib": "^2.4.0" @@ -32043,7 +32043,9 @@ } }, "node_modules/monaco-yaml": { - "version": "5.2.2", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.2.3.tgz", + "integrity": "sha512-GEplKC+YYmS0TOlJdv0FzbqkDN/IG2FSwEw+95myECVxTlhty2amwERYjzvorvJXmIagP1grd3Lleq7aQEJpPg==", "license": "MIT", "workspaces": [ "examples/*" diff --git a/client/package.json b/client/package.json index ad3ae618..4089383d 100644 --- a/client/package.json +++ b/client/package.json @@ -60,12 +60,12 @@ "buffer": "^6.0.3", "classnames": "^2.5.1", "dayjs": "^1.11.13", - "framer-motion": "^11.11.10", + "framer-motion": "^11.11.11", "lodash": "^4.17.21", "moment": "^2.30.1", "monaco-editor-webpack-plugin": "^7.1.0", "monaco-languageserver-types": "^0.4.0", - "monaco-yaml": "^5.2.2", + "monaco-yaml": "^5.2.3", "rc-banner-anim": "^2.4.5", "rc-menu": "^9.15.1", "rc-queue-anim": "^2.0.0", @@ -92,13 +92,13 @@ "@babel/preset-typescript": "^7.26.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.13.0", - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/classnames": "^2.3.4", "@types/express": "^5.0.0", "@types/history": "^5.0.0", "@types/jest": "^29.5.14", - "@types/lodash": "^4.17.12", + "@types/lodash": "^4.17.13", "@types/luxon": "^3.4.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", diff --git a/client/src/app.tsx b/client/src/app.tsx index 37679928..5607a252 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,3 +1,4 @@ +import AlertNotification from '@/components/Alert/AlertNotification'; import Footer from '@/components/Footer'; import { AvatarDropdown, @@ -116,6 +117,7 @@ export const layout: RunTimeLayoutConfig = ({ version != initialState?.currentUser?.settings?.server.version; return ( <> + {initialState?.currentUser?.settings?.server.version && versionMismatch && ( { + const [api, contextHolder] = notification.useNotification(); + const openNotificationWithIcon = (payload: any) => { + if (payload?.severity === 'error') { + api.error({ + message: 'Alert', + description: ( + + {payload?.message + ?.split('\n') + .map((line: string, index: number) =>

{line}

)} +
+ ), + duration: 0, + }); + } + }; + useEffect(() => { + socket.connect(); + socket.on(SsmEvents.Alert.NEW_ALERT, openNotificationWithIcon); + + return () => { + socket.off(SsmEvents.Alert.NEW_ALERT, openNotificationWithIcon); + socket.disconnect(); + }; + }, []); + return <>{contextHolder}; +}; + +export default AlertNotification; diff --git a/client/src/components/Icons/CustomIcons.tsx b/client/src/components/Icons/CustomIcons.tsx index 84ce1fb3..02df0dc7 100644 --- a/client/src/components/Icons/CustomIcons.tsx +++ b/client/src/components/Icons/CustomIcons.tsx @@ -2471,3 +2471,22 @@ const UpdateLineSvg = React.memo((props) => ( export const UpdateLine = (props: Partial) => ( ); + +const ErrorCircleSettings20RegularSvg = React.memo((props) => ( + + + +)); + +export const ErrorCircleSettings20Regular = ( + props: Partial, +) => ; diff --git a/client/src/components/NewDeviceModal/NewUnManagedDeviceModal.tsx b/client/src/components/NewDeviceModal/NewUnManagedDeviceModal.tsx index 3d9f20ee..9df956dc 100644 --- a/client/src/components/NewDeviceModal/NewUnManagedDeviceModal.tsx +++ b/client/src/components/NewDeviceModal/NewUnManagedDeviceModal.tsx @@ -10,6 +10,7 @@ import React from 'react'; export type NewUnManagedDeviceModalProps = { isModalOpen: boolean; setIsModalOpen: any; + onAddNewUnmanagedDevice: () => void; }; const { Text } = Typography; @@ -20,6 +21,7 @@ const NewUnManagedDeviceModal: React.FC = ( const handleCancel = () => { setDeviceUuid(undefined); props.setIsModalOpen(false); + props.onAddNewUnmanagedDevice(); }; const formRef = React.useRef(); diff --git a/client/src/pages/Admin/Inventory/index.tsx b/client/src/pages/Admin/Inventory/index.tsx index f3139320..83ff134a 100644 --- a/client/src/pages/Admin/Inventory/index.tsx +++ b/client/src/pages/Admin/Inventory/index.tsx @@ -126,6 +126,10 @@ const Inventory: React.FC = () => { }); }; + const onAddNewUnmanagedDevice = () => { + actionRef?.current?.reload(); + }; + const onDeleteNewDevice = async () => { if (currentRow) { await deleteDevice(currentRow?.uuid).then(() => { @@ -171,6 +175,7 @@ const Inventory: React.FC = () => { diff --git a/client/src/pages/Admin/Settings/Settings.tsx b/client/src/pages/Admin/Settings/Settings.tsx index 2ed851ce..0c144196 100644 --- a/client/src/pages/Admin/Settings/Settings.tsx +++ b/client/src/pages/Admin/Settings/Settings.tsx @@ -1,6 +1,7 @@ import Title, { TitleColors } from '@/components/Template/Title'; import AdvancedSettings from '@/pages/Admin/Settings/components/AdvancedSettings'; import AuthenticationSettings from '@/pages/Admin/Settings/components/AuthenticationSettings'; +import ContainerStacksSettings from '@/pages/Admin/Settings/components/ContainerStacksSettings'; import GeneralSettings from '@/pages/Admin/Settings/components/GeneralSettings'; import Information from '@/pages/Admin/Settings/components/Information'; import PlaybookSettings from '@/pages/Admin/Settings/components/PlaybooksSettings'; @@ -42,6 +43,15 @@ const Settings: React.FC = () => { ), children: , }, + { + key: 'container-stacks', + label: ( +
+ Container Stacks +
+ ), + children: , + }, { key: 'container-registries', label: ( diff --git a/client/src/pages/Admin/Settings/components/ContainerStacksSettings.tsx b/client/src/pages/Admin/Settings/components/ContainerStacksSettings.tsx new file mode 100644 index 00000000..55b3851e --- /dev/null +++ b/client/src/pages/Admin/Settings/components/ContainerStacksSettings.tsx @@ -0,0 +1,166 @@ +import { + ErrorCircleSettings20Regular, + SimpleIconsGit, +} from '@/components/Icons/CustomIcons'; +import Title, { TitleColors } from '@/components/Template/Title'; +import ContainerStacksGitRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal'; +import { getGitContainerStacksRepositories } from '@/services/rest/container-stacks-repositories'; +import { InfoCircleFilled } from '@ant-design/icons'; +import { ProList } from '@ant-design/pro-components'; +import { + Avatar, + Button, + Card, + Popover, + Space, + Tag, + Tooltip, + Typography, +} from 'antd'; +import { AddCircleOutline } from 'antd-mobile-icons'; +import React, { useEffect, useState } from 'react'; +import { API } from 'ssm-shared-lib'; + +const ContainerStacksSettings: React.FC = () => { + const [gitRepositories, setGitRepositories] = useState< + API.GitContainerStacksRepository[] + >([]); + + const asyncFetch = async () => { + await getGitContainerStacksRepositories().then((list) => { + if (list?.data) { + setGitRepositories(list.data); + } + }); + }; + + useEffect(() => { + void asyncFetch(); + }, []); + + const [gitModalOpened, setGitModalOpened] = useState(false); + const [selectedGitRecord, setSelectedGitRecord] = useState(); + + return ( + + + } + /> + } + extra={ + + + + + + + } + > + + ghost={true} + itemCardProps={{ + ghost: true, + }} + pagination={ + gitRepositories?.length > 8 + ? { + defaultPageSize: 8, + showSizeChanger: false, + showQuickJumper: false, + } + : false + } + rowSelection={false} + grid={{ gutter: 0, xs: 1, sm: 2, md: 2, lg: 2, xl: 4, xxl: 4 }} + onItem={(record: API.GitContainerStacksRepository) => { + return { + onMouseEnter: () => { + console.log(record); + }, + onClick: () => { + setSelectedGitRecord(record); + setGitModalOpened(true); + }, + }; + }} + metas={{ + title: { + dataIndex: 'name', + }, + subTitle: { + render: (_, row) => branch:{row.branch}, + }, + content: { + render: (_, row) => ( + + {row.userName}@{row.remoteUrl} + + ), + }, + type: {}, + avatar: { + render: () => } />, + }, + actions: { + cardActionProps: 'extra', + render: (_, row) => { + if (row.onError) { + return ( + + + This repository is on error: + + + {row.onErrorMessage} + + + } + > + + + ); + } + return undefined; + }, + }, + }} + dataSource={gitRepositories} + /> + + + ); +}; + +export default ContainerStacksSettings; diff --git a/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx b/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx index ad860236..7ecc2aef 100644 --- a/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx +++ b/client/src/pages/Admin/Settings/components/PlaybooksSettings.tsx @@ -1,13 +1,14 @@ import { + ErrorCircleSettings20Regular, SimpleIconsGit, StreamlineLocalStorageFolderSolid, } from '@/components/Icons/CustomIcons'; import Title, { TitleColors } from '@/components/Template/Title'; -import GitRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/GitRepositoryModal'; -import LocalRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/LocalRepositoryModal'; +import PlaybooksGitRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal'; +import PlaybooksLocalRepositoryModal from '@/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal'; import { - getGitRepositories, - getLocalRepositories, + getGitPlaybooksRepositories, + getPlaybooksLocalRepositories, } from '@/services/rest/playbooks-repositories'; import { postUserLogs } from '@/services/rest/usersettings'; import { useModel } from '@@/exports'; @@ -43,20 +44,20 @@ const PlaybookSettings: React.FC = () => { const [inputValue, setInputValue] = useState( currentUser?.settings.userSpecific.userLogsLevel.terminal, ); - const [gitRepositories, setGitRepositories] = useState( - [], - ); + const [gitRepositories, setGitRepositories] = useState< + API.GitPlaybooksRepository[] + >([]); const [localRepositories, setLocalRepositories] = useState< - API.LocalRepository[] + API.LocalPlaybooksRepository[] >([]); const asyncFetch = async () => { - await getGitRepositories().then((list) => { + await getGitPlaybooksRepositories().then((list) => { if (list?.data) { setGitRepositories(list.data); } }); - await getLocalRepositories().then((list) => { + await getPlaybooksLocalRepositories().then((list) => { if (list?.data) { setLocalRepositories(list.data); } @@ -86,14 +87,14 @@ const PlaybookSettings: React.FC = () => { return ( - - { } > - + ghost={true} itemCardProps={{ ghost: true, @@ -202,7 +203,7 @@ const PlaybookSettings: React.FC = () => { } rowSelection={false} grid={{ gutter: 0, xs: 1, sm: 2, md: 2, lg: 2, xl: 4, xxl: 4 }} - onItem={(record: API.LocalRepository) => { + onItem={(record: API.LocalPlaybooksRepository) => { return { onMouseEnter: () => { console.log(record); @@ -275,7 +276,7 @@ const PlaybookSettings: React.FC = () => { } > - + ghost={true} itemCardProps={{ ghost: true, @@ -291,7 +292,7 @@ const PlaybookSettings: React.FC = () => { } rowSelection={false} grid={{ gutter: 0, xs: 1, sm: 2, md: 2, lg: 2, xl: 4, xxl: 4 }} - onItem={(record: API.GitRepository) => { + onItem={(record: API.GitPlaybooksRepository) => { return { onMouseEnter: () => { console.log(record); @@ -322,6 +323,34 @@ const PlaybookSettings: React.FC = () => { }, actions: { cardActionProps: 'extra', + render: (_, row) => { + if (row.onError) { + return ( + + + This repository is on error: + + + {row.onErrorMessage} + + + } + > + + + ); + } + return undefined; + }, }, }} dataSource={gitRepositories} diff --git a/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx new file mode 100644 index 00000000..1e6d2a84 --- /dev/null +++ b/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx @@ -0,0 +1,205 @@ +import { SimpleIconsGit } from '@/components/Icons/CustomIcons'; +import FileMatchesForm from '@/pages/Admin/Settings/components/subcomponents/forms/FileMatchesForm'; +import GitForm from '@/pages/Admin/Settings/components/subcomponents/forms/GitForm'; +import { + commitAndSyncContainerStacksGitRepository, + deleteContainerStacksGitRepository, + forceCloneContainerStacksGitRepository, + forcePullContainerStacksGitRepository, + forceRegisterContainerStacksGitRepository, + postContainerStacksGitRepository, + putContainerStacksGitRepository, + syncToDatabaseContainerStacksGitRepository, +} from '@/services/rest/container-stacks-repositories'; +import { ModalForm, ProForm } from '@ant-design/pro-components'; +import { Avatar, Button, Dropdown, MenuProps, message } from 'antd'; +import React from 'react'; +import { API } from 'ssm-shared-lib'; + +type ContainerStacksGitRepositoryModalProps = { + selectedRecord: Partial; + modalOpened: boolean; + setModalOpened: any; + asyncFetch: () => Promise; + repositories: API.GitContainerStacksRepository[]; +}; + +const items = [ + { + key: '1', + label: 'Force Pull', + }, + { + key: '2', + label: 'Commit And Sync', + }, + { + key: '3', + label: 'Force Clone', + }, + { + key: '4', + label: 'Sync To Database', + }, + { + key: '5', + label: 'Force Register', + }, +]; + +const ContainerStacksGitRepositoryModal: React.FC< + ContainerStacksGitRepositoryModalProps +> = (props) => { + const onMenuClick: MenuProps['onClick'] = async (e) => { + if (!props.selectedRecord.uuid) { + message.error({ + content: 'Internal error - no uuid', + duration: 6, + }); + return; + } + switch (e.key) { + case '1': + await forcePullContainerStacksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Force pull command launched', + duration: 6, + }); + }); + return; + case '2': + await commitAndSyncContainerStacksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Commit and sync command launched', + duration: 6, + }); + }); + return; + case '3': + await forceCloneContainerStacksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Force clone command launched', + duration: 6, + }); + }); + return; + case '4': + await syncToDatabaseContainerStacksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Sync to database command launched', + duration: 6, + }); + }); + return; + case '5': + await forceRegisterContainerStacksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Force register command launched', + duration: 6, + }); + }); + return; + } + }; + + const editionMode = props.selectedRecord + ? [ + + Actions + , + , + ] + : []; + + return ( + + title={ + <> + } + /> + {(props.selectedRecord && ( + <>Edit repository {props.selectedRecord?.name} + )) || <>Add & sync a new repository} + + } + open={props.modalOpened} + autoFocusFirstInput + modalProps={{ + destroyOnClose: true, + onCancel: () => props.setModalOpened(false), + }} + onFinish={async (values) => { + if (props.selectedRecord) { + await postContainerStacksGitRepository( + props.selectedRecord.uuid as string, + values, + ); + props.setModalOpened(false); + await props.asyncFetch(); + } else { + await putContainerStacksGitRepository(values); + props.setModalOpened(false); + await props.asyncFetch(); + } + }} + submitter={{ + render: (_, defaultDoms) => { + return [...editionMode, ...defaultDoms]; + }, + }} + > + + + + + + ); +}; + +export default ContainerStacksGitRepositoryModal; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/GitRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx similarity index 52% rename from client/src/pages/Admin/Settings/components/subcomponents/GitRepositoryModal.tsx rename to client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx index 0575483c..308b7222 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/GitRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx @@ -1,26 +1,27 @@ import { SimpleIconsGit } from '@/components/Icons/CustomIcons'; -import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/DirectoryExclusionForm'; +import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm'; +import GitForm from '@/pages/Admin/Settings/components/subcomponents/forms/GitForm'; import { - commitAndSyncGitRepository, - deleteGitRepository, - forceCloneGitRepository, - forcePullGitRepository, - forceRegisterGitRepository, - postGitRepository, - putGitRepository, - syncToDatabaseGitRepository, + commitAndSyncPlaybooksGitRepository, + deletePlaybooksGitRepository, + forceClonePlaybooksGitRepository, + forcePullPlaybooksGitRepository, + forceRegisterPlaybooksGitRepository, + postPlaybooksGitRepository, + putPlaybooksGitRepository, + syncToDatabasePlaybooksGitRepository, } from '@/services/rest/playbooks-repositories'; -import { ModalForm, ProForm, ProFormText } from '@ant-design/pro-components'; +import { ModalForm, ProForm } from '@ant-design/pro-components'; import { Avatar, Button, Dropdown, MenuProps, message } from 'antd'; import React from 'react'; import { API } from 'ssm-shared-lib'; -type GitRepositoryModalProps = { - selectedRecord: Partial; +type PlaybooksGitRepositoryModalProps = { + selectedRecord: Partial; modalOpened: boolean; setModalOpened: any; asyncFetch: () => Promise; - repositories: API.GitRepository[]; + repositories: API.GitPlaybooksRepository[]; }; const items = [ @@ -46,7 +47,9 @@ const items = [ }, ]; -const GitRepositoryModal: React.FC = (props) => { +const PlaybooksGitRepositoryModal: React.FC< + PlaybooksGitRepositoryModalProps +> = (props) => { const onMenuClick: MenuProps['onClick'] = async (e) => { if (!props.selectedRecord.uuid) { message.error({ @@ -57,42 +60,50 @@ const GitRepositoryModal: React.FC = (props) => { } switch (e.key) { case '1': - await forcePullGitRepository(props.selectedRecord.uuid).then(() => { - message.success({ - content: 'Force pull command launched', - duration: 6, - }); - }); + await forcePullPlaybooksGitRepository(props.selectedRecord.uuid).then( + () => { + message.loading({ + content: 'Force pull command launched', + duration: 6, + }); + }, + ); return; case '2': - await commitAndSyncGitRepository(props.selectedRecord.uuid).then(() => { - message.success({ + await commitAndSyncPlaybooksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ content: 'Commit and sync command launched', duration: 6, }); }); return; case '3': - await forceCloneGitRepository(props.selectedRecord.uuid).then(() => { - message.success({ - content: 'Force clone command launched', - duration: 6, - }); - }); - return; - case '4': - await syncToDatabaseGitRepository(props.selectedRecord.uuid).then( + await forceClonePlaybooksGitRepository(props.selectedRecord.uuid).then( () => { - message.success({ - content: 'Sync to database command launched', + message.loading({ + content: 'Force clone command launched', duration: 6, }); }, ); return; + case '4': + await syncToDatabasePlaybooksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ + content: 'Sync to database command launched', + duration: 6, + }); + }); + return; case '5': - await forceRegisterGitRepository(props.selectedRecord.uuid).then(() => { - message.success({ + await forceRegisterPlaybooksGitRepository( + props.selectedRecord.uuid, + ).then(() => { + message.loading({ content: 'Force register command launched', duration: 6, }); @@ -115,7 +126,7 @@ const GitRepositoryModal: React.FC = (props) => { danger onClick={async () => { if (props.selectedRecord && props.selectedRecord.uuid) { - await deleteGitRepository(props.selectedRecord.uuid) + await deletePlaybooksGitRepository(props.selectedRecord.uuid) .then(() => message.warning({ content: 'Repository deleted', @@ -135,7 +146,7 @@ const GitRepositoryModal: React.FC = (props) => { : []; return ( - + title={ <> = (props) => { }} onFinish={async (values) => { if (props.selectedRecord) { - await postGitRepository(values); + await postPlaybooksGitRepository( + props.selectedRecord.uuid as string, + values, + ); props.setModalOpened(false); await props.asyncFetch(); } else { - await putGitRepository(values); + await putPlaybooksGitRepository(values); props.setModalOpened(false); await props.asyncFetch(); } @@ -175,61 +189,10 @@ const GitRepositoryModal: React.FC = (props) => { }, }} > - - e.name === value) === undefined - ) { - return Promise.resolve(); - } - return Promise.reject('Name already exists'); - }, - }, - ]} - /> - - - - - - + @@ -237,4 +200,4 @@ const GitRepositoryModal: React.FC = (props) => { ); }; -export default GitRepositoryModal; +export default PlaybooksGitRepositoryModal; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/LocalRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx similarity index 77% rename from client/src/pages/Admin/Settings/components/subcomponents/LocalRepositoryModal.tsx rename to client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx index fbbbd4ec..c25d0cf4 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/LocalRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksLocalRepositoryModal.tsx @@ -1,10 +1,10 @@ import { SimpleIconsGit } from '@/components/Icons/CustomIcons'; -import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/DirectoryExclusionForm'; +import DirectoryExclusionForm from '@/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm'; import { - deleteLocalRepository, - postLocalRepositories, - putLocalRepositories, - syncToDatabaseLocalRepository, + deletePlaybooksLocalRepository, + postPlaybooksLocalRepositories, + putPlaybooksLocalRepositories, + syncToDatabasePlaybooksLocalRepository, } from '@/services/rest/playbooks-repositories'; import { ModalForm, ProForm, ProFormText } from '@ant-design/pro-components'; import { Avatar, Button, Dropdown, MenuProps, message } from 'antd'; @@ -12,11 +12,11 @@ import React, { FC, useState } from 'react'; import { API } from 'ssm-shared-lib'; type LocalRepositoryModalProps = { - selectedRecord: Partial; + selectedRecord: Partial; modalOpened: boolean; setModalOpened: any; asyncFetch: () => Promise; - repositories: API.LocalRepository[]; + repositories: API.LocalPlaybooksRepository[]; }; const items = [ @@ -26,7 +26,9 @@ const items = [ }, ]; -const LocalRepositoryModal: FC = (props) => { +const PlaybooksLocalRepositoryModal: FC = ( + props, +) => { const [loading, setLoading] = useState(false); const onMenuClick: MenuProps['onClick'] = async (e) => { if (!props.selectedRecord.uuid) { @@ -38,14 +40,14 @@ const LocalRepositoryModal: FC = (props) => { } switch (e.key) { case '4': - await syncToDatabaseLocalRepository(props.selectedRecord.uuid).then( - () => { - message.success({ - content: 'Sync to database command launched', - duration: 6, - }); - }, - ); + await syncToDatabasePlaybooksLocalRepository( + props.selectedRecord.uuid, + ).then(() => { + message.success({ + content: 'Sync to database command launched', + duration: 6, + }); + }); return; } }; @@ -66,7 +68,7 @@ const LocalRepositoryModal: FC = (props) => { onClick={async () => { setLoading(true); if (props.selectedRecord && props.selectedRecord.uuid) { - await deleteLocalRepository(props.selectedRecord.uuid) + await deletePlaybooksLocalRepository(props.selectedRecord.uuid) .then(() => message.warning({ content: 'Repository deleted', @@ -86,7 +88,7 @@ const LocalRepositoryModal: FC = (props) => { ] : []; return ( - + title={ <> = (props) => { onFinish={async (values) => { if (!props.selectedRecord?.default) { if (props.selectedRecord) { - await postLocalRepositories({ - ...props.selectedRecord, - name: values.name, - directoryExclusionList: values.directoryExclusionList, - }); + await postPlaybooksLocalRepositories( + props.selectedRecord.uuid as string, + { + ...props.selectedRecord, + name: values.name, + directoryExclusionList: values.directoryExclusionList, + }, + ); props.setModalOpened(false); await props.asyncFetch(); } else { - await putLocalRepositories(values); + await putPlaybooksLocalRepositories(values); props.setModalOpened(false); await props.asyncFetch(); } @@ -166,4 +171,4 @@ const LocalRepositoryModal: FC = (props) => { ); }; -export default LocalRepositoryModal; +export default PlaybooksLocalRepositoryModal; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/DirectoryExclusionForm.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx similarity index 100% rename from client/src/pages/Admin/Settings/components/subcomponents/DirectoryExclusionForm.tsx rename to client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx diff --git a/client/src/pages/Admin/Settings/components/subcomponents/forms/FileMatchesForm.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/FileMatchesForm.tsx new file mode 100644 index 00000000..a632c439 --- /dev/null +++ b/client/src/pages/Admin/Settings/components/subcomponents/forms/FileMatchesForm.tsx @@ -0,0 +1,52 @@ +import { ProFormSelect } from '@ant-design/pro-form'; +import { message } from 'antd'; +import React, { useState } from 'react'; +import { API } from 'ssm-shared-lib'; + +type FileMatchesFormProps = { + selectedRecord: Partial; +}; + +const FileMatchesForm: React.FC = (props) => { + const [tags, setTags] = useState( + props.selectedRecord?.matchesList || [ + 'docker-compose.yml', + 'docker-compose.yaml', + ], + ); + + const validateTag = (tag: string): boolean => { + const pattern = /^[^\\/]+$/; + return pattern.test(tag); + }; + + const handleTagsChange = (newTags: string[]) => { + const invalidTag = newTags.find((tag) => !validateTag(tag)); + if (invalidTag) { + void message.error('Characters / and \\ are not authorized'); + } else { + setTags(newTags); + } + }; + + return ( + <> + + + ); +}; + +export default FileMatchesForm; diff --git a/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx new file mode 100644 index 00000000..9243ed3a --- /dev/null +++ b/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx @@ -0,0 +1,73 @@ +import { ProForm, ProFormText } from '@ant-design/pro-components'; +import React from 'react'; +import { API } from 'ssm-shared-lib'; + +export type GitFormProps = { + selectedRecord: Partial< + API.GitPlaybooksRepository | API.GitContainerStacksRepository + >; + repositories: + | API.GitPlaybooksRepository[] + | API.GitContainerStacksRepository[]; +}; + +const GitForm: React.FC = ({ selectedRecord, repositories }) => ( + + e.name === value) === undefined || + selectedRecord?.name === value + ) { + return Promise.resolve(); + } + return Promise.reject('Name already exists'); + }, + }, + ]} + /> + + + + + + +); + +export default GitForm; diff --git a/client/src/pages/ComposeEditor/index.tsx b/client/src/pages/ComposeEditor/index.tsx index b2eec24c..6c7d3b29 100644 --- a/client/src/pages/ComposeEditor/index.tsx +++ b/client/src/pages/ComposeEditor/index.tsx @@ -30,7 +30,7 @@ import { import { ProFormSelect } from '@ant-design/pro-form'; import Editor, { Monaco } from '@monaco-editor/react'; import { useSearchParams } from '@umijs/max'; -import type { InputRef } from 'antd'; +import { InputRef, Tag } from 'antd'; import { Alert, Button, Col, message, notification, Row, Space } from 'antd'; import { AnimatePresence, motion } from 'framer-motion'; import { editor } from 'monaco-editor'; @@ -329,6 +329,7 @@ const ComposeEditor = () => { optionRender: (option) => ( + {option.data.type} { icon: e.icon, iconColor: e.iconColor, iconBackgroundColor: e.iconBackgroundColor, + type: e.type, }; }); }) diff --git a/client/src/pages/Playbooks/components/PlaybookDropdownMenu.tsx b/client/src/pages/Playbooks/components/PlaybookDropdownMenu.tsx index 302c2408..a92a291a 100644 --- a/client/src/pages/Playbooks/components/PlaybookDropdownMenu.tsx +++ b/client/src/pages/Playbooks/components/PlaybookDropdownMenu.tsx @@ -1,7 +1,7 @@ import { Callbacks } from '@/pages/Playbooks/components/DirectoryTreeView'; import { - commitAndSyncGitRepository, - forcePullGitRepository, + commitAndSyncPlaybooksGitRepository, + forcePullPlaybooksGitRepository, } from '@/services/rest/playbooks-repositories'; import { ArrowDownOutlined, @@ -109,17 +109,19 @@ const PlaybookDropdownMenu: React.FC = (props) => { setOpen(true); break; case '4': - await commitAndSyncGitRepository(props.playbookRepository.uuid).then( - () => { - message.info({ - content: 'Commit & sync command sent', - duration: 6, - }); - }, - ); + await commitAndSyncPlaybooksGitRepository( + props.playbookRepository.uuid, + ).then(() => { + message.info({ + content: 'Commit & sync command sent', + duration: 6, + }); + }); break; case '5': - await forcePullGitRepository(props.playbookRepository.uuid).then(() => { + await forcePullPlaybooksGitRepository( + props.playbookRepository.uuid, + ).then(() => { message.info({ content: 'Force pull command sent', duration: 6 }); }); break; diff --git a/client/src/pages/Playbooks/components/TreeComponent.tsx b/client/src/pages/Playbooks/components/TreeComponent.tsx index f6c2b72a..df53d055 100644 --- a/client/src/pages/Playbooks/components/TreeComponent.tsx +++ b/client/src/pages/Playbooks/components/TreeComponent.tsx @@ -7,7 +7,7 @@ import { API, DirectoryTree, DirectoryTree as DT, - Playbooks, + Repositories, } from 'ssm-shared-lib'; export type ClientPlaybooksTrees = { @@ -33,7 +33,7 @@ export function buildTree( ): ClientPlaybooksTrees { return { _name: rootNode.name, - remoteRootNode: rootNode.type === Playbooks.PlaybooksRepositoryType.GIT, + remoteRootNode: rootNode.type === Repositories.RepositoryType.GIT, depth: 0, rootNode: true, playbookRepository: { @@ -44,7 +44,7 @@ export function buildTree( key: rootNode.name, nodeType: DT.CONSTANTS.DIRECTORY, icon: - rootNode.type === Playbooks.PlaybooksRepositoryType.LOCAL ? ( + rootNode.type === Repositories.RepositoryType.LOCAL ? ( ) : ( diff --git a/client/src/services/rest/container-stacks-repositories.ts b/client/src/services/rest/container-stacks-repositories.ts new file mode 100644 index 00000000..8128d353 --- /dev/null +++ b/client/src/services/rest/container-stacks-repositories.ts @@ -0,0 +1,151 @@ +import { request } from '@umijs/max'; +import { API } from 'ssm-shared-lib'; + +export async function getGitContainerStacksRepositories( + params?: any, + options?: Record, +) { + return request>( + `/api/container-repository/git/`, + { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function postContainerStacksGitRepository( + repositoryUuid: string, + repository: API.GitContainerStacksRepository, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${repositoryUuid}`, + { + data: { ...repository }, + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function putContainerStacksGitRepository( + repository: API.GitContainerStacksRepository, + params?: any, + options?: Record, +) { + return request(`/api/container-repository/git/`, { + data: { ...repository }, + method: 'PUT', + params: { + ...params, + }, + ...(options || {}), + }); +} + +export async function deleteContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request(`/api/container-repository/git/${uuid}`, { + method: 'DELETE', + params: { + ...params, + }, + ...(options || {}), + }); +} + +export async function syncToDatabaseContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${uuid}/sync-to-database-repository`, + { + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function forcePullContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${uuid}/force-pull-repository`, + { + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function forceCloneContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${uuid}/force-clone-repository`, + { + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function commitAndSyncContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${uuid}/commit-and-sync-repository`, + { + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +export async function forceRegisterContainerStacksGitRepository( + uuid: string, + params?: any, + options?: Record, +) { + return request( + `/api/container-repository/git/${uuid}/force-register`, + { + method: 'POST', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} diff --git a/client/src/services/rest/playbooks-repositories.ts b/client/src/services/rest/playbooks-repositories.ts index 47d743c2..b0412e3a 100644 --- a/client/src/services/rest/playbooks-repositories.ts +++ b/client/src/services/rest/playbooks-repositories.ts @@ -1,7 +1,9 @@ import { request } from '@umijs/max'; import { API } from 'ssm-shared-lib'; -export async function getPlaybooksRepositories(): Promise { +export async function getPlaybooksRepositories(): Promise< + API.Response +> { return request>( '/api/playbooks-repository/', { @@ -11,11 +13,11 @@ export async function getPlaybooksRepositories(): Promise, ) { - return request>( + return request>( `/api/playbooks-repository/git/`, { method: 'GET', @@ -27,11 +29,11 @@ export async function getGitRepositories( ); } -export async function getLocalRepositories( +export async function getPlaybooksLocalRepositories( params?: any, options?: Record, ) { - return request>( + return request>( `/api/playbooks-repository/local/`, { method: 'GET', @@ -43,13 +45,14 @@ export async function getLocalRepositories( ); } -export async function postLocalRepositories( - repository: Partial, +export async function postPlaybooksLocalRepositories( + repositoryUuid: string, + repository: Partial, params?: any, options?: Record, ) { - return request>( - `/api/playbooks-repository/local/${repository.uuid}`, + return request>( + `/api/playbooks-repository/local/${repositoryUuid}`, { data: { ...repository }, method: 'POST', @@ -61,12 +64,12 @@ export async function postLocalRepositories( ); } -export async function putLocalRepositories( - repository: API.LocalRepository, +export async function putPlaybooksLocalRepositories( + repository: API.LocalPlaybooksRepository, params?: any, options?: Record, ) { - return request>( + return request>( `/api/playbooks-repository/local/`, { data: { ...repository }, @@ -79,7 +82,7 @@ export async function putLocalRepositories( ); } -export async function deleteLocalRepository( +export async function deletePlaybooksLocalRepository( uuid: string, params?: any, options?: Record, @@ -93,7 +96,7 @@ export async function deleteLocalRepository( }); } -export async function syncToDatabaseLocalRepository( +export async function syncToDatabasePlaybooksLocalRepository( uuid: string, params?: any, options?: Record, @@ -110,13 +113,14 @@ export async function syncToDatabaseLocalRepository( ); } -export async function postGitRepository( - repository: API.GitRepository, +export async function postPlaybooksGitRepository( + repositoryUuid: string, + repository: API.GitPlaybooksRepository, params?: any, options?: Record, ) { return request( - `/api/playbooks-repository/git/${repository.uuid}`, + `/api/playbooks-repository/git/${repositoryUuid}`, { data: { ...repository }, method: 'POST', @@ -128,8 +132,8 @@ export async function postGitRepository( ); } -export async function putGitRepository( - repository: API.GitRepository, +export async function putPlaybooksGitRepository( + repository: API.GitPlaybooksRepository, params?: any, options?: Record, ) { @@ -143,7 +147,7 @@ export async function putGitRepository( }); } -export async function deleteGitRepository( +export async function deletePlaybooksGitRepository( uuid: string, params?: any, options?: Record, @@ -157,7 +161,7 @@ export async function deleteGitRepository( }); } -export async function syncToDatabaseGitRepository( +export async function syncToDatabasePlaybooksGitRepository( uuid: string, params?: any, options?: Record, @@ -174,7 +178,7 @@ export async function syncToDatabaseGitRepository( ); } -export async function forcePullGitRepository( +export async function forcePullPlaybooksGitRepository( uuid: string, params?: any, options?: Record, @@ -191,7 +195,7 @@ export async function forcePullGitRepository( ); } -export async function forceCloneGitRepository( +export async function forceClonePlaybooksGitRepository( uuid: string, params?: any, options?: Record, @@ -208,7 +212,7 @@ export async function forceCloneGitRepository( ); } -export async function commitAndSyncGitRepository( +export async function commitAndSyncPlaybooksGitRepository( uuid: string, params?: any, options?: Record, @@ -225,7 +229,7 @@ export async function commitAndSyncGitRepository( ); } -export async function forceRegisterGitRepository( +export async function forceRegisterPlaybooksGitRepository( uuid: string, params?: any, options?: Record, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 97306cb8..ccb7a5ea 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -51,8 +51,7 @@ services: - ./.env.dev volumes: - ./server/src:/opt/squirrelserversmanager/server/src - - ./.data.dev/playbooks:/playbooks - - ./.data.dev/config:/ansible-config + - ./.data.dev:/data environment: NODE_ENV: development DEBUG: nodejs-docker-express:* diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4628c428..4c7dfa89 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -39,8 +39,7 @@ services: - mongo - redis volumes: - - ./.data.prod/playbooks:/playbooks - - ./.data.prod/config:/ansible-config + - ./.data.prod:/data build: context: ./server additional_contexts: diff --git a/docker-compose.yml b/docker-compose.yml index b1532228..946e6675 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,8 +36,7 @@ services: environment: NODE_ENV: production volumes: - - ./.data.prod/playbooks:/playbooks - - ./.data.prod/config:/ansible-config + - ./.data.prod:/data client: image: "ghcr.io/squirrelcorporation/squirrelserversmanager-client:latest" restart: unless-stopped diff --git a/server/package-lock.json b/server/package-lock.json index 11f97ac1..c8a60b99 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -28,7 +28,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.5.0", - "mongoose": "^8.7.3", + "mongoose": "^8.8.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "node-ssh": "^13.2.0", @@ -54,8 +54,8 @@ "devDependencies": { "@eslint/compat": "^1.2.2", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.13.0", - "@stylistic/eslint-plugin": "^2.9.0", + "@eslint/js": "^9.14.0", + "@stylistic/eslint-plugin": "^2.10.1", "@types/bcrypt": "^5.0.2", "@types/cookie-parser": "^1.4.7", "@types/dockerode": "^3.3.31", @@ -81,7 +81,7 @@ "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", "@vitest/coverage-v8": "^2.1.4", - "eslint": "^9.13.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import-x": "^4.4.0", "eslint-plugin-prettier": "^5.2.1", @@ -1840,9 +1840,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1924,9 +1924,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", - "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1968,25 +1968,40 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", - "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", - "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.0", + "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2023,9 +2038,10 @@ "license": "BSD-3-Clause" }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", + "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -3079,14 +3095,15 @@ "license": "MIT" }, "node_modules/@stylistic/eslint-plugin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.9.0.tgz", - "integrity": "sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.10.1.tgz", + "integrity": "sha512-U+4yzNXElTf9q0kEfnloI9XbOyD4cnEQCxjUI94q0+W++0GAEQvJ/slwEj9lwjDHfGADRSr+Tco/z0XJvmDfCQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.8.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "@typescript-eslint/utils": "^8.12.2", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "estraverse": "^5.3.0", "picomatch": "^4.0.2" }, @@ -3098,10 +3115,11 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4029,9 +4047,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5261,21 +5279,21 @@ } }, "node_modules/eslint": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", - "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", + "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.13.0", + "@eslint/js": "9.14.0", "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5283,9 +5301,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5411,9 +5429,10 @@ } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -5438,9 +5457,10 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5449,13 +5469,14 @@ } }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5465,9 +5486,10 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7211,9 +7233,9 @@ "license": "MIT" }, "node_modules/mongodb": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", - "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", + "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.5", @@ -7340,14 +7362,14 @@ } }, "node_modules/mongoose": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", - "integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.0.tgz", + "integrity": "sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==", "license": "MIT", "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.9.0", + "mongodb": "~6.10.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", diff --git a/server/package.json b/server/package.json index 30caa7c2..54b24dc8 100644 --- a/server/package.json +++ b/server/package.json @@ -34,7 +34,7 @@ "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.5.0", - "mongoose": "^8.7.3", + "mongoose": "^8.8.0", "node-cron": "^3.0.3", "node-ssh": "^13.2.0", "parse-docker-image-name": "^3.0.0", @@ -65,7 +65,7 @@ "devDependencies": { "@eslint/compat": "^1.2.2", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.13.0", + "@eslint/js": "^9.14.0", "@types/bcrypt": "^5.0.2", "@types/cookie-parser": "^1.4.7", "@types/dockerode": "^3.3.31", @@ -88,11 +88,11 @@ "@types/multer": "^1.4.12", "@typescript-eslint/eslint-plugin": "^8.12.2", "@vitest/coverage-v8": "^2.1.4", - "eslint": "^9.13.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-import-x": "^4.4.0", - "@stylistic/eslint-plugin": "^2.9.0", + "@stylistic/eslint-plugin": "^2.10.1", "@typescript-eslint/parser": "^8.12.2", "globals": "^15.11.0", "mongodb-memory-server": "^10.1.2", diff --git a/server/src/config.ts b/server/src/config.ts index 9bede031..45cd1715 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -16,3 +16,5 @@ export const redisConf = { export const SECRET = process.env.SECRET || ''; export const VAULT_PWD = process.env.VAULT_PWD || ''; export const SESSION_DURATION = parseInt(process.env.SESSION_DURATION || '86400000'); +export const SSM_INSTALL_PATH = process.env.SSM_INSTALL_PATH || '/opt/squirrelserversmanager'; +export const SSM_DATA_PATH = process.env.SSM_DATA_PATH || '/data'; diff --git a/server/src/controllers/rest/containers-stacks-repository/git.ts b/server/src/controllers/rest/containers-stacks-repository/git.ts new file mode 100644 index 00000000..7d0d8807 --- /dev/null +++ b/server/src/controllers/rest/containers-stacks-repository/git.ts @@ -0,0 +1,154 @@ +import ContainerCustomStackRepositoryRepo from '../../../data/database/repository/ContainerCustomStackRepositoryRepo'; +import { NotFoundError } from '../../../middlewares/api/ApiError'; +import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; +import { DEFAULT_VAULT_ID, vaultEncrypt } from '../../../modules/ansible-vault/ansible-vault'; +import ContainerCustomStacksRepositoryComponent from '../../../modules/repository/ContainerCustomStacksRepositoryComponent'; +import ContainerCustomStacksRepositoryEngine from '../../../modules/repository/ContainerCustomStacksRepositoryEngine'; +import GitRepositoryUseCases from '../../../services/GitCustomStacksRepositoryUseCases'; + +export const addGitRepository = async (req, res) => { + const { + name, + accessToken, + branch, + email, + userName, + remoteUrl, + matchesList, + }: { + name: string; + accessToken: string; + branch: string; + email: string; + userName: string; + remoteUrl: string; + matchesList?: string[]; + } = req.body; + await GitRepositoryUseCases.addGitRepository( + name, + await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + branch, + email, + userName, + remoteUrl, + matchesList, + ); + new SuccessResponse('Added container stacks git repository').send(res); +}; + +export const getGitRepositories = async (req, res) => { + const repositories = await ContainerCustomStackRepositoryRepo.findAllActive(); + const encryptedRepositories = repositories?.map((repo) => ({ + ...repo, + accessToken: 'REDACTED', + })); + new SuccessResponse('Got container stacks git repositories', encryptedRepositories).send(res); +}; + +export const updateGitRepository = async (req, res) => { + const { uuid } = req.params; + const { + name, + accessToken, + branch, + email, + gitUserName, + remoteUrl, + matchesList, + }: { + name: string; + accessToken: string; + branch: string; + email: string; + gitUserName: string; + remoteUrl: string; + matchesList?: string[]; + } = req.body; + await GitRepositoryUseCases.updateGitRepository( + uuid, + name, + await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + branch, + email, + gitUserName, + remoteUrl, + matchesList, + ); + new SuccessResponse('Updated container stacks git repository').send(res); +}; + +export const deleteGitRepository = async (req, res) => { + const { uuid } = req.params; + + const repository = await ContainerCustomStackRepositoryRepo.findOneByUuid(uuid); + if (!repository) { + throw new NotFoundError(); + } + await GitRepositoryUseCases.deleteRepository(repository); + new SuccessResponse('Deleted container stacks repository').send(res); +}; + +export const forcePullRepository = async (req, res) => { + const { uuid } = req.params; + + const repository = ContainerCustomStacksRepositoryEngine.getState().stackRepository[ + uuid + ] as ContainerCustomStacksRepositoryComponent; + if (!repository) { + throw new NotFoundError(); + } + await repository.forcePull(); + await repository.syncToDatabase(); + new SuccessResponse('Forced pull stacks git repository').send(res); +}; + +export const forceCloneRepository = async (req, res) => { + const { uuid } = req.params; + const repository = ContainerCustomStacksRepositoryEngine.getState().stackRepository[ + uuid + ] as ContainerCustomStacksRepositoryComponent; + if (!repository) { + throw new NotFoundError(); + } + + await repository.clone(); + await repository.syncToDatabase(); + new SuccessResponse('Forced cloned stacks git repository').send(res); +}; + +export const commitAndSyncRepository = async (req, res) => { + const { uuid } = req.params; + const repository = ContainerCustomStacksRepositoryEngine.getState().stackRepository[ + uuid + ] as ContainerCustomStacksRepositoryComponent; + if (!repository) { + throw new NotFoundError(); + } + + await repository.commitAndSync(); + new SuccessResponse('Commit And Synced stacks git repository').send(res); +}; + +export const syncToDatabaseRepository = async (req, res) => { + const { uuid } = req.params; + const repository = ContainerCustomStacksRepositoryEngine.getState().stackRepository[ + uuid + ] as ContainerCustomStacksRepositoryComponent; + if (!repository) { + throw new NotFoundError(); + } + + await repository.syncToDatabase(); + new SuccessResponse('Synced to database stacks git repository').send(res); +}; + +export const forceRegister = async (req, res) => { + const { uuid } = req.params; + const repository = await ContainerCustomStackRepositoryRepo.findOneByUuid(uuid); + if (!repository) { + throw new NotFoundError(); + } + + await ContainerCustomStacksRepositoryEngine.registerRepository(repository); + new SuccessResponse('Synced to database stacks git repository').send(res); +}; diff --git a/server/src/controllers/rest/containers-stacks-repository/git.validator.ts b/server/src/controllers/rest/containers-stacks-repository/git.validator.ts new file mode 100644 index 00000000..00efc0fc --- /dev/null +++ b/server/src/controllers/rest/containers-stacks-repository/git.validator.ts @@ -0,0 +1,30 @@ +import { body, param } from 'express-validator'; +import validator from '../../../middlewares/Validator'; + +export const addGitRepositoryValidator = [ + body('name').exists().isString().withMessage('Name is incorrect'), + body('accessToken').exists().isString().withMessage('Access token is incorrect'), + body('branch').exists().isString().withMessage('Branch is incorrect'), + body('email').exists().isEmail().withMessage('Email is incorrect'), + body('userName').exists().isString().withMessage('userName is incorrect'), + body('remoteUrl').exists().isURL().withMessage('remoteUrl is incorrect'), + body('matchesList').exists().isArray().withMessage('matchesList is incorrect'), + validator, +]; + +export const updateGitRepositoryValidator = [ + param('uuid').exists().isString().isUUID().withMessage('Uuid is incorrect'), + body('name').exists().isString().withMessage('Name is incorrect'), + body('accessToken').exists().isString().withMessage('Access token is incorrect'), + body('branch').exists().isString().withMessage('Branch is incorrect'), + body('email').exists().isEmail().withMessage('Email is incorrect'), + body('userName').exists().isString().withMessage('userName is incorrect'), + body('remoteUrl').exists().isURL().withMessage('remoteUrl is incorrect'), + body('matchesList').exists().isArray().withMessage('matchesListis incorrect'), + validator, +]; + +export const genericGitRepositoryActionValidator = [ + param('uuid').exists().isString().withMessage('Uuid is incorrect'), + validator, +]; diff --git a/server/src/controllers/rest/containers/stacks.ts b/server/src/controllers/rest/containers/stacks.ts index 40ad35ba..2fe91399 100644 --- a/server/src/controllers/rest/containers/stacks.ts +++ b/server/src/controllers/rest/containers/stacks.ts @@ -1,4 +1,5 @@ import { parse } from 'url'; +import { RepositoryType } from 'ssm-shared-lib/distribution/enums/repositories'; import { API } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import ContainerCustomStackRepo from '../../../data/database/repository/ContainerCustomStackRepo'; @@ -16,7 +17,7 @@ import PlaybookUseCases from '../../../services/PlaybookUseCases'; export const getCustomStacks = async (req, res) => { const realUrl = req.url; - const { current = 1, pageSize = 10 } = req.query; + const { current, pageSize } = req.query; const params = parse(realUrl, true).query as unknown as API.PageParams & API.ContainerCustomStack & { sorter: any; @@ -29,8 +30,9 @@ export const getCustomStacks = async (req, res) => { dataSource = filterByFields(dataSource, params); dataSource = filterByQueryParams(dataSource, params, ['uuid', 'name']); const totalBeforePaginate = dataSource?.length || 0; - dataSource = paginate(dataSource, current as number, pageSize as number); - + if (current && pageSize) { + dataSource = paginate(dataSource, current as number, pageSize as number); + } new SuccessResponse('Got Custom Stacks', dataSource, { total: totalBeforePaginate, success: true, @@ -121,6 +123,9 @@ export const patchCustomStack = async (req, res) => { iconColor, iconBackgroundColor, }); + if (stack.type === RepositoryType.GIT) { + FileSystemManager.writeFile(yaml, stack.path as string); + } new SuccessResponse('Put Custom Stack', stack).send(res); } }; diff --git a/server/src/controllers/rest/playbooks-repository/git.ts b/server/src/controllers/rest/playbooks-repository/git.ts index 83620979..5e2418a0 100644 --- a/server/src/controllers/rest/playbooks-repository/git.ts +++ b/server/src/controllers/rest/playbooks-repository/git.ts @@ -1,11 +1,11 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import PlaybooksRepositoryRepo from '../../../data/database/repository/PlaybooksRepositoryRepo'; import { NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import { DEFAULT_VAULT_ID, vaultEncrypt } from '../../../modules/ansible-vault/ansible-vault'; -import GitRepositoryComponent from '../../../modules/playbooks-repository/git-repository/GitRepositoryComponent'; -import PlaybooksRepositoryEngine from '../../../modules/playbooks-repository/PlaybooksRepositoryEngine'; -import GitRepositoryUseCases from '../../../services/GitRepositoryUseCases'; +import GitPlaybooksRepositoryComponent from '../../../modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent'; +import PlaybooksRepositoryEngine from '../../../modules/repository/PlaybooksRepositoryEngine'; +import GitRepositoryUseCases from '../../../services/GitPlaybooksRepositoryUseCases'; import PlaybooksRepositoryUseCases from '../../../services/PlaybooksRepositoryUseCases'; export const addGitRepository = async (req, res) => { @@ -40,7 +40,7 @@ export const addGitRepository = async (req, res) => { export const getGitRepositories = async (req, res) => { const repositories = await PlaybooksRepositoryRepo.findAllWithType( - Playbooks.PlaybooksRepositoryType.GIT, + Repositories.RepositoryType.GIT, ); const encryptedRepositories = repositories?.map((repo) => ({ ...repo, @@ -98,7 +98,7 @@ export const forcePullRepository = async (req, res) => { const repository = PlaybooksRepositoryEngine.getState().playbooksRepository[ uuid - ] as GitRepositoryComponent; + ] as GitPlaybooksRepositoryComponent; if (!repository) { throw new NotFoundError(); } @@ -111,7 +111,7 @@ export const forceCloneRepository = async (req, res) => { const { uuid } = req.params; const repository = PlaybooksRepositoryEngine.getState().playbooksRepository[ uuid - ] as GitRepositoryComponent; + ] as GitPlaybooksRepositoryComponent; if (!repository) { throw new NotFoundError(); } @@ -125,7 +125,7 @@ export const commitAndSyncRepository = async (req, res) => { const { uuid } = req.params; const repository = PlaybooksRepositoryEngine.getState().playbooksRepository[ uuid - ] as GitRepositoryComponent; + ] as GitPlaybooksRepositoryComponent; if (!repository) { throw new NotFoundError(); } @@ -138,7 +138,7 @@ export const syncToDatabaseRepository = async (req, res) => { const { uuid } = req.params; const repository = PlaybooksRepositoryEngine.getState().playbooksRepository[ uuid - ] as GitRepositoryComponent; + ] as GitPlaybooksRepositoryComponent; if (!repository) { throw new NotFoundError(); } diff --git a/server/src/controllers/rest/playbooks-repository/local.ts b/server/src/controllers/rest/playbooks-repository/local.ts index 909d1212..2423a1db 100644 --- a/server/src/controllers/rest/playbooks-repository/local.ts +++ b/server/src/controllers/rest/playbooks-repository/local.ts @@ -1,17 +1,17 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import PlaybooksRepositoryRepo from '../../../data/database/repository/PlaybooksRepositoryRepo'; import logger from '../../../logger'; import { NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; -import LocalRepositoryComponent from '../../../modules/playbooks-repository/local-repository/LocalRepositoryComponent'; -import PlaybooksRepositoryEngine from '../../../modules/playbooks-repository/PlaybooksRepositoryEngine'; -import LocalRepositoryUseCases from '../../../services/LocalRepositoryUseCases'; +import LocalPlaybooksRepositoryComponent from '../../../modules/repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent'; +import PlaybooksRepositoryEngine from '../../../modules/repository/PlaybooksRepositoryEngine'; +import LocalRepositoryUseCases from '../../../services/LocalPlaybooksRepositoryUseCases'; import PlaybooksRepositoryUseCases from '../../../services/PlaybooksRepositoryUseCases'; export const getLocalRepositories = async (req, res) => { logger.info(`[CONTROLLER] - GET - /local/`); const repositories = await PlaybooksRepositoryRepo.findAllWithType( - Playbooks.PlaybooksRepositoryType.LOCAL, + Repositories.RepositoryType.LOCAL, ); new SuccessResponse('Got playbooks local repositories', repositories).send(res); }; @@ -59,7 +59,7 @@ export const syncToDatabaseLocalRepository = async (req, res) => { const { uuid } = req.params; const repository = PlaybooksRepositoryEngine.getState().playbooksRepository[ uuid - ] as LocalRepositoryComponent; + ] as LocalPlaybooksRepositoryComponent; if (!repository) { throw new NotFoundError(); } diff --git a/server/src/controllers/rest/settings/advanced.ts b/server/src/controllers/rest/settings/advanced.ts index 3d87deef..b0a4fc31 100644 --- a/server/src/controllers/rest/settings/advanced.ts +++ b/server/src/controllers/rest/settings/advanced.ts @@ -8,7 +8,7 @@ import DeviceStatRepo from '../../../data/database/repository/DeviceStatRepo'; import LogsRepo from '../../../data/database/repository/LogsRepo'; import { restart } from '../../../index'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; -import PlaybooksRepositoryEngine from '../../../modules/playbooks-repository/PlaybooksRepositoryEngine'; +import PlaybooksRepositoryEngine from '../../../modules/repository/PlaybooksRepositoryEngine'; export const postRestartServer = async (req, res) => { await restart(); diff --git a/server/src/controllers/rest/user/user.ts b/server/src/controllers/rest/user/user.ts index c9b6b71d..a73b811f 100644 --- a/server/src/controllers/rest/user/user.ts +++ b/server/src/controllers/rest/user/user.ts @@ -6,7 +6,7 @@ import { Role } from '../../../data/database/model/User'; import UserRepo from '../../../data/database/repository/UserRepo'; import { AuthFailureError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; -import { createADefaultLocalUserRepository } from '../../../modules/playbooks-repository/default-repositories'; +import { createADefaultLocalUserRepository } from '../../../modules/repository/default-playbooks-repositories'; import DashboardUseCase from '../../../services/DashboardUseCase'; import DeviceUseCases from '../../../services/DeviceUseCases'; diff --git a/server/src/core/events/events.ts b/server/src/core/events/events.ts index c3e909de..d7c6dbf2 100644 --- a/server/src/core/events/events.ts +++ b/server/src/core/events/events.ts @@ -5,6 +5,7 @@ enum Events { APP_STARTED = 'APP_STARTED', UPDATED_CONTAINERS = 'UPDATED_CONTAINERS', UPDATED_NOTIFICATIONS = 'UPDATED_NOTIFICATIONS', + ALERT = 'ALERT', } export default Events; diff --git a/server/src/core/startup/index.ts b/server/src/core/startup/index.ts index 200b2eb6..5035b30b 100644 --- a/server/src/core/startup/index.ts +++ b/server/src/core/startup/index.ts @@ -1,6 +1,7 @@ -import { SettingsKeys } from 'ssm-shared-lib'; +import { Repositories, SettingsKeys } from 'ssm-shared-lib'; import { getFromCache, setToCache } from '../../data/cache'; import initRedisValues from '../../data/cache/defaults'; +import { ContainerCustomStackModel } from '../../data/database/model/ContainerCustomStack'; import { DeviceModel } from '../../data/database/model/Device'; import { PlaybookModel } from '../../data/database/model/Playbook'; import { copyAnsibleCfgFileIfDoesntExist } from '../../helpers/ansible/AnsibleConfigurationHelper'; @@ -10,8 +11,9 @@ import Crons from '../../modules/crons'; import WatcherEngine from '../../modules/docker/core/WatcherEngine'; import providerConf from '../../modules/docker/registries/providers/provider.conf'; import NotificationComponent from '../../modules/notifications/NotificationComponent'; -import { createADefaultLocalUserRepository } from '../../modules/playbooks-repository/default-repositories'; -import PlaybooksRepositoryEngine from '../../modules/playbooks-repository/PlaybooksRepositoryEngine'; +import ContainerCustomStacksRepositoryEngine from '../../modules/repository/ContainerCustomStacksRepositoryEngine'; +import { createADefaultLocalUserRepository } from '../../modules/repository/default-playbooks-repositories'; +import PlaybooksRepositoryEngine from '../../modules/repository/PlaybooksRepositoryEngine'; import UpdateChecker from '../../modules/update/UpdateChecker'; import ContainerRegistryUseCases from '../../services/ContainerRegistryUseCases'; import DeviceAuthUseCases from '../../services/DeviceAuthUseCases'; @@ -44,6 +46,7 @@ class Startup { void WatcherEngine.init(); void AutomationEngine.init(); void UpdateChecker.checkVersion(); + void ContainerCustomStacksRepositoryEngine.init(); } private async updateScheme() { @@ -57,6 +60,10 @@ class Startup { this.registerPersistedProviders(); copyAnsibleCfgFileIfDoesntExist(); await setToCache('_ssm_masterNodeUrl', (await getFromCache('ansible-master-node-url')) || ''); + await ContainerCustomStackModel.updateMany( + { type: { $exists: false } }, + { $set: { type: Repositories.RepositoryType.LOCAL } }, + ); } private isSchemeVersionDifferent(schemeVersion: string | null): boolean { diff --git a/server/src/data/database/model/ContainerCustomStack.ts b/server/src/data/database/model/ContainerCustomStack.ts index 8f82feed..a41ff698 100644 --- a/server/src/data/database/model/ContainerCustomStack.ts +++ b/server/src/data/database/model/ContainerCustomStack.ts @@ -1,5 +1,7 @@ import { Schema, model } from 'mongoose'; import { v4 as uuidv4 } from 'uuid'; +import { Repositories } from 'ssm-shared-lib'; +import ContainerCustomStackRepository from './ContainerCustomStackRepository'; export const DOCUMENT_NAME = 'ContainerCustomStack'; export const COLLECTION_NAME = 'containercustomstacks'; @@ -10,10 +12,13 @@ export default interface ContainerCustomStack { iconColor?: string; iconBackgroundColor?: string; name: string; - json: any; + json?: any; yaml: string; - rawStackValue: any; + rawStackValue?: any; lockJson: boolean; + type: Repositories.RepositoryType; + path?: string; + containerCustomStackRepository?: ContainerCustomStackRepository; } const schema = new Schema( @@ -26,7 +31,6 @@ const schema = new Schema( }, name: { type: Schema.Types.String, - unique: true, }, json: { type: Object, @@ -50,6 +54,21 @@ const schema = new Schema( iconBackgroundColor: { type: Schema.Types.String, }, + path: { + type: Schema.Types.String, + }, + type: { + type: Schema.Types.String, + required: true, + default: Repositories.RepositoryType.LOCAL, + }, + containerCustomStackRepository: { + type: Schema.Types.ObjectId, + ref: 'ContainerCustomStackRepository', + required: false, + select: true, + index: true, + }, }, { timestamps: true, diff --git a/server/src/data/database/model/ContainerCustomStackRepository.ts b/server/src/data/database/model/ContainerCustomStackRepository.ts new file mode 100644 index 00000000..ecd5cd82 --- /dev/null +++ b/server/src/data/database/model/ContainerCustomStackRepository.ts @@ -0,0 +1,81 @@ +import { Schema, model } from 'mongoose'; + +export const DOCUMENT_NAME = 'ContainerCustomStackRepository'; +export const COLLECTION_NAME = 'containercustomstackssrepository'; + +export default interface ContainerCustomStackRepository { + _id?: string; + uuid: string; + name: string; + accessToken: string; + branch: string; + email: string; + userName: string; + remoteUrl: string; + enabled: boolean; + matchesList?: string[]; + createdAt?: Date; + updatedAt?: Date; + onError?: boolean; + onErrorMessage?: string; +} + +const schema = new Schema( + { + uuid: { + type: Schema.Types.String, + required: true, + unique: true, + }, + name: { + type: Schema.Types.String, + required: true, + }, + accessToken: { + type: Schema.Types.String, + required: false, + }, + branch: { + type: Schema.Types.String, + required: false, + }, + email: { + type: Schema.Types.String, + required: false, + }, + userName: { + type: Schema.Types.String, + required: false, + }, + remoteUrl: { + type: Schema.Types.String, + required: false, + }, + enabled: { + type: Schema.Types.Boolean, + required: true, + default: true, + }, + matchesList: { + type: Schema.Types.Array, + }, + onError: { + type: Schema.Types.Boolean, + default: false, + }, + onErrorMessage: { + type: Schema.Types.String, + required: false, + }, + }, + { + timestamps: true, + versionKey: false, + }, +); + +export const ContainerCustomStacksRepositoryModel = model( + DOCUMENT_NAME, + schema, + COLLECTION_NAME, +); diff --git a/server/src/data/database/model/PlaybooksRepository.ts b/server/src/data/database/model/PlaybooksRepository.ts index afa187f2..01f27fb9 100644 --- a/server/src/data/database/model/PlaybooksRepository.ts +++ b/server/src/data/database/model/PlaybooksRepository.ts @@ -1,5 +1,5 @@ import { Schema, model } from 'mongoose'; -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; export const DOCUMENT_NAME = 'PlaybooksRepository'; export const COLLECTION_NAME = 'playbooksrepository'; @@ -7,7 +7,7 @@ export const COLLECTION_NAME = 'playbooksrepository'; export default interface PlaybooksRepository { _id?: string; uuid: string; - type: Playbooks.PlaybooksRepositoryType; + type: Repositories.RepositoryType; name: string; accessToken?: string; branch?: string; @@ -19,6 +19,8 @@ export default interface PlaybooksRepository { default?: boolean; tree?: any; directoryExclusionList?: string[]; + onError?: boolean; + onErrorMessage?: string; createdAt?: Date; updatedAt?: Date; } @@ -89,6 +91,14 @@ const schema = new Schema( 'inventories', ], }, + onError: { + type: Schema.Types.Boolean, + default: false, + }, + onErrorMessage: { + type: Schema.Types.String, + required: false, + }, }, { timestamps: true, diff --git a/server/src/data/database/repository/ContainerCustomStackRepo.ts b/server/src/data/database/repository/ContainerCustomStackRepo.ts index f3c6b08f..4c40dd70 100644 --- a/server/src/data/database/repository/ContainerCustomStackRepo.ts +++ b/server/src/data/database/repository/ContainerCustomStackRepo.ts @@ -1,4 +1,5 @@ import ContainerCustomStack, { ContainerCustomStackModel } from '../model/ContainerCustomStack'; +import ContainerCustomStackRepository from '../model/ContainerCustomStackRepository'; async function findAll() { return await ContainerCustomStackModel.find().lean().exec(); @@ -25,10 +26,26 @@ async function deleteOne(uuid: string) { await ContainerCustomStackModel.deleteOne({ uuid: uuid }).exec(); } +async function listAllByRepository( + containerCustomStackRepository: ContainerCustomStackRepository, +): Promise { + return await ContainerCustomStackModel.find({ + containerCustomStackRepository: containerCustomStackRepository, + }) + .lean() + .exec(); +} + +async function findOneByPath(path: string) { + return await ContainerCustomStackModel.findOne({ path: path }).lean().exec(); +} + export default { findAll, updateOrCreate, deleteOne, findByName, findByUuid, + listAllByRepository, + findOneByPath, }; diff --git a/server/src/data/database/repository/ContainerCustomStackRepositoryRepo.ts b/server/src/data/database/repository/ContainerCustomStackRepositoryRepo.ts new file mode 100644 index 00000000..01a90afa --- /dev/null +++ b/server/src/data/database/repository/ContainerCustomStackRepositoryRepo.ts @@ -0,0 +1,41 @@ +import ContainerCustomStackRepository, { + ContainerCustomStacksRepositoryModel, +} from '../model/ContainerCustomStackRepository'; + +async function create( + containerCustomStackRepository: ContainerCustomStackRepository, +): Promise { + const created = await ContainerCustomStacksRepositoryModel.create(containerCustomStackRepository); + return created.toObject(); +} + +async function findAllActive(): Promise { + return await ContainerCustomStacksRepositoryModel.find({ enabled: true }).lean().exec(); +} + +async function findOneByUuid(uuid: string): Promise { + return await ContainerCustomStacksRepositoryModel.findOne({ uuid: uuid }).lean().exec(); +} + +async function update( + containerCustomStackRepository: ContainerCustomStackRepository, +): Promise { + containerCustomStackRepository.updatedAt = new Date(); + return ContainerCustomStacksRepositoryModel.findOneAndUpdate( + { uuid: containerCustomStackRepository.uuid }, + containerCustomStackRepository, + ) + .lean() + .exec(); +} + +async function deleteByUuid(uuid: string): Promise { + await ContainerCustomStacksRepositoryModel.deleteOne({ uuid: uuid }).exec(); +} +export default { + create, + findAllActive, + findOneByUuid, + update, + deleteByUuid, +}; diff --git a/server/src/data/database/repository/PlaybooksRepositoryRepo.ts b/server/src/data/database/repository/PlaybooksRepositoryRepo.ts index b57dea81..c57f7d1b 100644 --- a/server/src/data/database/repository/PlaybooksRepositoryRepo.ts +++ b/server/src/data/database/repository/PlaybooksRepositoryRepo.ts @@ -1,4 +1,4 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import PlaybooksRepository, { PlaybooksRepositoryModel } from '../model/PlaybooksRepository'; async function update( @@ -31,7 +31,7 @@ async function create(playbooksRepository: PlaybooksRepository): Promise { return await PlaybooksRepositoryModel.find({ enabled: true, type: type }).lean().exec(); } @@ -41,7 +41,7 @@ async function findAllActive(): Promise { } async function findAllWithType( - type: Playbooks.PlaybooksRepositoryType, + type: Repositories.RepositoryType, ): Promise { return await PlaybooksRepositoryModel.find({ type: type }).lean().exec(); } diff --git a/server/src/helpers/ansible/AnsibleConfigurationHelper.ts b/server/src/helpers/ansible/AnsibleConfigurationHelper.ts index b2830f1a..8d74d0fb 100644 --- a/server/src/helpers/ansible/AnsibleConfigurationHelper.ts +++ b/server/src/helpers/ansible/AnsibleConfigurationHelper.ts @@ -1,7 +1,8 @@ import fs from 'fs'; +import { SSM_DATA_PATH, SSM_INSTALL_PATH } from '../../config'; import FileSystemManager from '../../modules/shell/managers/FileSystemManager'; -export const ANSIBLE_CONFIG_FILE = '/ansible-config/ansible.cfg'; +export const ANSIBLE_CONFIG_FILE = `${SSM_DATA_PATH}/config/ansible.cfg`; interface ConfigEntry { value: string; @@ -18,7 +19,7 @@ interface Config { export const copyAnsibleCfgFileIfDoesntExist = () => { if (!FileSystemManager.test('-f', ANSIBLE_CONFIG_FILE)) { FileSystemManager.copyFile( - '/opt/squirrelserversmanager/server/src/ansible/default-ansible.cfg', + `${SSM_INSTALL_PATH}/server/src/ansible/default-ansible.cfg`, ANSIBLE_CONFIG_FILE, ); } diff --git a/server/src/helpers/files/recursive-find.ts b/server/src/helpers/files/recursive-find.ts new file mode 100644 index 00000000..bf331fc4 --- /dev/null +++ b/server/src/helpers/files/recursive-find.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +type SearchPattern = string | RegExp; + +export type FileInfo = { + fullPath: string; + name: string; +}; + +export function getMatchingFiles(dir: string, patterns: SearchPattern[]): FileInfo[] { + const results: FileInfo[] = []; + + function searchDirectory(directory: string) { + const entries = fs.readdirSync(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isFile()) { + if ( + patterns.some( + (pattern) => + (typeof pattern === 'string' && entry.name.includes(pattern)) || + (pattern instanceof RegExp && pattern.test(entry.name)), + ) + ) { + results.push({ + fullPath, + name: entry.name, + }); + } + } else if (entry.isDirectory()) { + searchDirectory(fullPath); + } + } + } + + searchDirectory(dir); + return results; +} diff --git a/server/src/helpers/git/CREDIT.md b/server/src/helpers/git/CREDIT.md new file mode 100644 index 00000000..bad05604 --- /dev/null +++ b/server/src/helpers/git/CREDIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 lin onetwo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/src/helpers/git/commitAndSync.ts b/server/src/helpers/git/commitAndSync.ts index dccc03be..2a646671 100644 --- a/server/src/helpers/git/commitAndSync.ts +++ b/server/src/helpers/git/commitAndSync.ts @@ -1,5 +1,4 @@ import { GitProcess } from 'dugite'; -import myLogger from '../../logger'; import { credentialOff, credentialOn } from './credential'; import { defaultGitInfo as defaultDefaultGitInfo } from './defaultGitInfo'; import { @@ -39,7 +38,7 @@ export interface ICommitAndSyncOptions { userInfo?: IGitUserInfos; } /** - * `playbooks-repository add .` + `playbooks-repository commit` + `playbooks-repository rebase` or something that can sync bi-directional + * `git add .` + `git commit` + `git rebase` or something that can sync bi-directional */ export async function commitAndSync(options: ICommitAndSyncOptions): Promise { const { @@ -194,7 +193,7 @@ export async function commitAndSync(options: ICommitAndSyncOptions): Promise { if (options?.bare === true) { - const bareGitPath = path.join(dir, '.playbooks-repository'); + const bareGitPath = path.join(dir, '.git'); await fs.mkdirp(bareGitPath); await GitProcess.exec(['init', `--initial-branch=${branch}`, '--bare'], bareGitPath); } else { @@ -43,7 +43,7 @@ export async function initGitWithBranch( if (options?.initialCommit !== false) { await GitProcess.exec( - ['commit', `--allow-empty`, '-n', '-m', 'Initial commit when init a new playbooks-repository.'], + ['commit', `--allow-empty`, '-n', '-m', 'Initial commit when init a new git repository.'], dir, ); } diff --git a/server/src/helpers/git/initGit.ts b/server/src/helpers/git/initGit.ts index 47ce243f..4ee54e24 100644 --- a/server/src/helpers/git/initGit.ts +++ b/server/src/helpers/git/initGit.ts @@ -14,7 +14,7 @@ export interface IInitGitOptionsSyncImmediately { logger?: ILogger; /** only required if syncImmediately is true, the storage service url we are sync to, for example your github repo url */ remoteUrl: string; - /** should we sync after playbooks-repository init? */ + /** should we sync after git repository init? */ syncImmediately: true; /** user info used in the commit message */ userInfo: IGitUserInfos; @@ -24,7 +24,7 @@ export interface IInitGitOptionsNotSync { /** folder path, can be relative */ dir: string; logger?: ILogger; - /** should we sync after playbooks-repository init? */ + /** should we sync after git repository init? */ syncImmediately?: false; userInfo?: IGitUserInfosWithoutToken | IGitUserInfos; } @@ -55,7 +55,7 @@ export async function initGit(options: IInitGitOptions): Promise { logDebug(`Successfully Running git init in dir ${dir}`, GitStep.StartGitInitialization); await commitFiles(dir, gitUserName, email ?? defaultGitInfo.email); - // if we are config local note playbooks-repository, we are done here + // if we are config local note git repository, we are done here if (syncImmediately !== true) { logProgress(GitStep.GitRepositoryConfigurationFinished); return; diff --git a/server/src/helpers/git/inspect.ts b/server/src/helpers/git/inspect.ts index b172eccd..3edac200 100644 --- a/server/src/helpers/git/inspect.ts +++ b/server/src/helpers/git/inspect.ts @@ -7,7 +7,7 @@ import fs from 'fs-extra'; import { compact } from 'lodash'; import { AssumeSyncError, CantSyncGitNotInitializedError } from './errors'; import { GitStep, ILogger } from './interface'; -// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-require-imports const { listRemotes } = require('isomorphic-git'); const gitEscapeToEncodedUri = (str: string): string => @@ -25,7 +25,7 @@ export interface ModifiedFileList { } /** * Get modified files and modify type in a folder - * @param {string} folderPath location to scan playbooks-repository modify state + * @param {string} folderPath location to scan git repository modify state */ export async function getModifiedFileList(folderPath: string): Promise { const { stdout } = await GitProcess.exec(['status', '--porcelain'], folderPath); @@ -73,10 +73,10 @@ export async function getModifiedFileList(folderPath: string): Promise { const { stdout } = await GitProcess.exec(['status', '--porcelain'], folderPath); const matchResult = stdout.match(/^(\?\?|[ACMR] |[ ACMR][DM])*/gm); - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + return !!matchResult?.some?.(Boolean); } /** - * Get "master" or "main" from playbooks-repository repo + * Get "master" or "main" from git repository repo * * https://github.com/simonthum/git-sync/blob/31cc140df2751e09fae2941054d5b61c34e8b649/git-sync#L228-L232 * @param folderPath @@ -142,7 +142,7 @@ export async function getDefaultBranchName(folderPath: string): Promise { if (!(await hasGit(folderPath))) { @@ -272,7 +272,7 @@ export async function getGitRepositoryState(folderPath: string, logger?: ILogger )?.isFile?.(), ]); let result = ''; - /* eslint-disable @typescript-eslint/strict-boolean-expressions */ + if (isRebaseI) { result += 'REBASE-i'; } else if (isRebaseM) { @@ -304,7 +304,7 @@ export async function getGitRepositoryState(folderPath: string, logger?: ILogger result += '|DIRTY'; } } */ - // previous above `playbooks-repository diff --no-ext-diff --quiet --exit-code` logic from playbooks-repository-sync script can only detect if an existed file changed, can't detect newly added file, so we use `haveLocalChanges` instead + // previous above `git diff --no-ext-diff --quiet --exit-code` logic from git-sync script can only detect if an existed file changed, can't detect newly added file, so we use `haveLocalChanges` instead if (await haveLocalChanges(folderPath)) { result += '|DIRTY'; } @@ -313,7 +313,7 @@ export async function getGitRepositoryState(folderPath: string, logger?: ILogger } /** - * echo the playbooks-repository dir + * echo the git repository dir * @param dir repo path * @param logger */ @@ -337,10 +337,7 @@ export async function getGitDirectory(dir: string, logger?: ILogger): Promise { diff --git a/server/src/helpers/git/sync.ts b/server/src/helpers/git/sync.ts index 811b8997..f89936ce 100644 --- a/server/src/helpers/git/sync.ts +++ b/server/src/helpers/git/sync.ts @@ -173,7 +173,7 @@ export async function continueRebase( rebaseContinueStdError = rebaseContinueResult.stderr; const rebaseContinueStdOut = rebaseContinueResult.stdout; repositoryState = await getGitRepositoryState(dir, logger); - // if playbooks-repository add . + playbooks-repository commit failed or playbooks-repository rebase --continue failed + // if git add . + git commit failed or git rebase --continue failed if (commitExitCode !== 0 || rebaseContinueExitCode !== 0) { throw new CantSyncInSpecialGitStateAutoFixFailed( `rebaseContinueStdError when ${repositoryState}: ${rebaseContinueStdError}\ncommitStdError when ${repositoryState}: ${commitStdError}\n${rebaseContinueStdError}`, @@ -186,7 +186,7 @@ export async function continueRebase( } /** - * Simply calling playbooks-repository fetch. + * Simply calling git fetch. * @param dir * @param remoteName * @param branch if not provided, will fetch all branches diff --git a/server/src/modules/real-time/RealTime.ts b/server/src/modules/real-time/RealTime.ts index a71f70cc..e8f85b66 100644 --- a/server/src/modules/real-time/RealTime.ts +++ b/server/src/modules/real-time/RealTime.ts @@ -19,6 +19,12 @@ const eventsToHandle = [ logMessage: 'Notifications updated', debounceTime: 5000, }, + { + event: Events.ALERT, + ssmEvent: SsmEvents.Alert.NEW_ALERT, + logMessage: 'Alert sent', + debounceTime: 5000, + }, // Add any additional events here ]; @@ -38,10 +44,10 @@ class RealTimeEngine extends EventManager { } private createDebouncedEmitter(eventName: string, logMessage: string, debounceTime: number) { - return debounce(() => { + return debounce((payload: any) => { const io = App.getSocket().getIo(); - this.childLogger.debug(`${logMessage}`); - io.emit(eventName); + this.childLogger.info(`${logMessage}`); + io.emit(eventName, payload); }, debounceTime); } @@ -50,8 +56,11 @@ class RealTimeEngine extends EventManager { this.childLogger.info('init...'); eventsToHandle.forEach(({ event, ssmEvent, logMessage, debounceTime }) => { + this.childLogger.debug( + `Registering event ${event} with ssmEvent ${ssmEvent} and debounceTime ${debounceTime}`, + ); const debouncedEmitter = this.createDebouncedEmitter(ssmEvent, logMessage, debounceTime); - this.on(event, debouncedEmitter); + this.on(event, (payload: any) => debouncedEmitter(payload)); }); } catch (error: any) { this.childLogger.error(error); diff --git a/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts new file mode 100644 index 00000000..801fe5e6 --- /dev/null +++ b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts @@ -0,0 +1,290 @@ +import pino from 'pino'; +import shell from 'shelljs'; +import { RepositoryType } from 'ssm-shared-lib/distribution/enums/repositories'; +import { SsmAlert } from 'ssm-shared-lib'; +import { v4 as uuidv4 } from 'uuid'; +import { SSM_DATA_PATH } from '../../config'; +import EventManager from '../../core/events/EventManager'; +import Events from '../../core/events/events'; +import ContainerCustomStack from '../../data/database/model/ContainerCustomStack'; +import ContainerCustomStackRepository from '../../data/database/model/ContainerCustomStackRepository'; +import ContainerCustomStackRepo from '../../data/database/repository/ContainerCustomStackRepo'; +import ContainerStacksRepositoryRepo from '../../data/database/repository/ContainerCustomStackRepositoryRepo'; +import { FileInfo, getMatchingFiles } from '../../helpers/files/recursive-find'; +import logger from '../../logger'; +import { NotFoundError } from '../../middlewares/api/ApiError'; +import GitCustomStacksRepositoryUseCases from '../../services/GitCustomStacksRepositoryUseCases'; +import Shell from '../shell'; +import FileSystemManager from '../shell/managers/FileSystemManager'; +import { + GitStep, + IGitUserInfos, + IInitGitOptionsSyncImmediately, + ILoggerContext, + clone, + commitAndSync, + forcePull, +} from '../../helpers/git'; + +export const DIRECTORY_ROOT = `${SSM_DATA_PATH}/container-stacks`; + +class ContainerCustomStacksRepositoryComponent extends EventManager { + public name: string; + public directory: string; + public uuid: string; + public childLogger: pino.Logger; + private readonly options: IInitGitOptionsSyncImmediately; + + constructor( + uuid: string, + name: string, + branch: string, + email: string, + gitUserName: string, + accessToken: string, + remoteUrl: string, + ) { + super(); + const dir = `${DIRECTORY_ROOT}/${uuid}`; + this.uuid = uuid; + this.directory = dir; + this.name = name; + this.childLogger = logger.child( + { + module: `ContainerCustomStackRepository`, + moduleId: `${this.uuid}`, + moduleName: `${this.name}`, + }, + { msgPrefix: `[CONTAINER_CUSTOM_STACK_GIT_REPOSITORY] - ` }, + ); + const userInfo: IGitUserInfos = { + email: email, + gitUserName: gitUserName, + branch: branch, + accessToken: accessToken, + }; + this.options = { + dir: this.directory, + syncImmediately: true, + userInfo: userInfo, + remoteUrl: remoteUrl, + }; + } + + public async delete() { + Shell.FileSystemManager.deleteFiles(this.directory); + } + + public async save(containerStackUuid: string, content: string) { + const containerStack = await ContainerCustomStackRepo.findByUuid(containerStackUuid); + if (!containerStack) { + throw new NotFoundError(`Container Stack ${containerStackUuid} not found`); + } + shell.ShellString(content).to(containerStack.path as string); + } + + public async syncToDatabase() { + this.childLogger.info('saving to database...'); + const containerStackRepository = await this.getContainerStackRepository(); + const containerStacksListFromDatabase = + await ContainerCustomStackRepo.listAllByRepository(containerStackRepository); + this.childLogger.info( + `Found ${containerStacksListFromDatabase?.length || 0} stacks from database`, + ); + const containerStacksListFromDirectory = getMatchingFiles( + this.directory, + containerStackRepository.matchesList as string[], + ); + this.childLogger.debug(containerStacksListFromDirectory); + this.childLogger.info( + `Found ${containerStacksListFromDirectory?.length || 0} stacks from directory`, + ); + const containerStacksListToDelete = containerStacksListFromDatabase?.filter((stack) => { + return !containerStacksListFromDirectory?.some((p) => p.fullPath === stack.path); + }); + this.childLogger.info( + `Found ${containerStacksListToDelete?.length || 0} stacks to delete from database`, + ); + if (containerStacksListToDelete && containerStacksListToDelete.length > 0) { + await Promise.all( + containerStacksListToDelete?.map((stack) => { + if (stack && stack.uuid) { + return ContainerCustomStackRepo.deleteOne(stack.uuid); + } + }), + ); + } + const containerStackPathsToSync = containerStacksListFromDirectory?.filter((stack) => { + return stack !== undefined; + }) as FileInfo[]; + this.childLogger.info(`Stacks to sync : ${containerStackPathsToSync.length}`); + await Promise.all( + containerStackPathsToSync.map(async (stackPath) => { + return this.updateOrCreateAssociatedStack(stackPath, containerStackRepository); + }), + ); + this.childLogger.info(`Updating Stacks Repository ${containerStackRepository.name}`); + } + + private async getContainerStackRepository() { + const containerStacksRepository = await ContainerStacksRepositoryRepo.findOneByUuid(this.uuid); + if (!containerStacksRepository) { + throw new NotFoundError(`Container Stacks repository ${this.uuid} not found`); + } + return containerStacksRepository; + } + + private async updateOrCreateAssociatedStack( + foundStack: FileInfo, + containerCustomStackRepository: ContainerCustomStackRepository, + ): Promise { + const stackFoundInDatabase = await ContainerCustomStackRepo.findOneByPath( + foundStack.fullPath as string, + ); + this.childLogger.debug( + `Processing stack ${JSON.stringify(foundStack)} - In database: ${stackFoundInDatabase ? 'true' : 'false'}`, + ); + const stackContent = FileSystemManager.readFile(foundStack.fullPath as string); + const stackData: ContainerCustomStack = { + path: foundStack.fullPath, + name: + stackFoundInDatabase?.name || + foundStack.fullPath.split(`${this.uuid}/`)[1].replaceAll('/', '_').toLowerCase(), + containerCustomStackRepository: containerCustomStackRepository, + uuid: stackFoundInDatabase?.uuid || uuidv4(), + lockJson: true, + type: RepositoryType.GIT, + yaml: stackContent, + icon: stackFoundInDatabase?.icon || 'file', + iconBackgroundColor: stackFoundInDatabase?.iconBackgroundColor || '#000000', + iconColor: stackFoundInDatabase?.iconColor || '#ffffff', + }; + this.childLogger.debug(`Stack data: ${JSON.stringify(stackData)}`); + await ContainerCustomStackRepo.updateOrCreate(stackData); + } + + public fileBelongToRepository(path: string) { + this.childLogger.info( + `rootPath: ${this.directory?.split('/')[0]} versus ${path.split('/')[0]}`, + ); + return this.directory?.split('/')[0] === path.split('/')[0]; + } + + getDirectory() { + return this.directory; + } + + async clone(syncAfter: boolean = false) { + this.childLogger.info('Clone starting...'); + try { + await GitCustomStacksRepositoryUseCases.resetRepositoryError(this.uuid); + try { + void Shell.FileSystemManager.createDirectory(this.directory, DIRECTORY_ROOT); + } catch (error: any) { + logger.warn(error); + } + await clone({ + ...this.options, + logger: { + debug: (message: string, context: ILoggerContext): unknown => + this.childLogger.debug(message, { callerFunction: 'clone', ...context }), + warn: (message: string, context: ILoggerContext): unknown => + this.childLogger.warn(message, { callerFunction: 'clone', ...context }), + info: (message: GitStep, context: ILoggerContext): void => { + this.childLogger.info(message, { + callerFunction: 'clone', + ...context, + }); + }, + }, + }); + if (syncAfter) { + await this.syncToDatabase(); + } + } catch (error: any) { + this.childLogger.error(error); + await GitCustomStacksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.childLogger.info(`Emit ${Events.ALERT} with error: ${error.message}`); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during git clone: ${error.message}`, + module: 'ContainerCustomStackRepository', + }); + } + } + + async commitAndSync() { + try { + await GitCustomStacksRepositoryUseCases.resetRepositoryError(this.uuid); + await commitAndSync({ + ...this.options, + logger: { + debug: (message: string, context: ILoggerContext): unknown => + this.childLogger.debug(message, { callerFunction: 'commitAndSync', ...context }), + warn: (message: string, context: ILoggerContext): unknown => + this.childLogger.warn(message, { callerFunction: 'commitAndSync', ...context }), + info: (message: GitStep, context: ILoggerContext): void => { + this.childLogger.info(message, { + callerFunction: 'commitAndSync', + ...context, + }); + }, + }, + }); + } catch (error: any) { + this.childLogger.error(error); + await GitCustomStacksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during commit and sync: ${error.message}`, + module: 'ContainerCustomStackRepository', + }); + } + } + + async forcePull() { + try { + await GitCustomStacksRepositoryUseCases.resetRepositoryError(this.uuid); + await forcePull({ + ...this.options, + logger: { + debug: (message: string, context: ILoggerContext): unknown => + this.childLogger.debug(message, { callerFunction: 'forcePull', ...context }), + warn: (message: string, context: ILoggerContext): unknown => + this.childLogger.warn(message, { callerFunction: 'forcePull', ...context }), + info: (message: GitStep, context: ILoggerContext): void => { + this.childLogger.info(message, { + callerFunction: 'forcePull', + ...context, + }); + }, + }, + }); + } catch (error: any) { + this.childLogger.error(error); + await GitCustomStacksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during force pull: ${error.message}`, + module: 'ContainerCustomStackRepository', + }); + } + } + + async init() { + await this.clone(); + } + + async syncFromRepository() { + await this.forcePull(); + } +} + +export interface AbstractComponent extends ContainerCustomStacksRepositoryComponent { + save(playbookUuid: string, content: string): Promise; + init(): Promise; + delete(): Promise; + syncFromRepository(): Promise; +} + +export default ContainerCustomStacksRepositoryComponent; diff --git a/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts b/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts new file mode 100644 index 00000000..c67c4b05 --- /dev/null +++ b/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts @@ -0,0 +1,115 @@ +import ContainerCustomStackRepository from '../../data/database/model/ContainerCustomStackRepository'; +import ContainerCustomStackRepositoryRepo from '../../data/database/repository/ContainerCustomStackRepositoryRepo'; +import PinoLogger from '../../logger'; +import { DEFAULT_VAULT_ID, vaultDecrypt } from '../ansible-vault/ansible-vault'; +import ContainerCustomStacksRepositoryComponent from './ContainerCustomStacksRepositoryComponent'; + +const logger = PinoLogger.child( + { module: 'ContainerCustomStacksRepositoryEngine' }, + { msgPrefix: '[CONTAINER_CUSTOM_STACKS_REPOSITORY_ENGINE] - ' }, +); + +type stateType = { + stackRepository: ContainerCustomStacksRepositoryComponent[]; +}; + +const state: stateType = { + stackRepository: [], +}; + +/** + * Return all registered repositories + * @returns {*} + */ +export function getState(): stateType { + return state; +} + +async function registerGitRepository( + containerCustomStackRepository: ContainerCustomStackRepository, +) { + const { uuid, name, branch, email, userName, accessToken, remoteUrl } = + containerCustomStackRepository; + if (!accessToken) { + throw new Error('accessToken is required'); + } + const decryptedAccessToken = await vaultDecrypt(accessToken, DEFAULT_VAULT_ID); + if (!decryptedAccessToken) { + throw new Error('Error decrypting access token'); + } + return new ContainerCustomStacksRepositoryComponent( + uuid, + name, + branch, + email, + userName, + decryptedAccessToken, + remoteUrl, + ); +} + +async function registerRepository(containerCustomStackRepository: ContainerCustomStackRepository) { + logger.info( + `Registering ${containerCustomStackRepository.name}/${containerCustomStackRepository.uuid}`, + ); + state.stackRepository[containerCustomStackRepository.uuid] = await registerGitRepository( + containerCustomStackRepository, + ); + return state.stackRepository[ + containerCustomStackRepository.uuid + ] as ContainerCustomStacksRepositoryComponent; +} + +async function registerRepositories() { + const repos = await ContainerCustomStackRepositoryRepo.findAllActive(); + logger.info(`Found ${repos?.length} active repositories`); + const repositoriesToRegister: any = []; + repos?.map((repo) => { + repositoriesToRegister.push(registerRepository(repo)); + }); + await Promise.all(repositoriesToRegister); +} + +async function deregisterRepository(uuid: string) { + const repository = getState().stackRepository[uuid]; + if (!repository) { + throw new Error('Repository not found'); + } + delete state.stackRepository[uuid]; +} + +async function clone(uuid: string) { + const gitRepository = getState().stackRepository[uuid]; + if (!gitRepository) { + throw new Error("Repository not registered / doesn't exist"); + } + await gitRepository.clone(); +} + +async function init() { + try { + await registerRepositories(); + } catch (error) { + logger.fatal('Error during initialization, your system may not be stable'); + logger.fatal(error); + } +} + +async function syncAllRegistered() { + logger.warn(`syncAllRegistered, ${getState().stackRepository.length} registered`); + await Promise.all( + Object.values(getState().stackRepository).map((component) => { + return component.syncFromRepository(); + }), + ); +} + +export default { + registerRepositories, + syncAllRegistered, + registerRepository, + clone, + init, + deregisterRepository, + getState, +}; diff --git a/server/src/modules/playbooks-repository/PlaybooksRepositoryComponent.ts b/server/src/modules/repository/PlaybooksRepositoryComponent.ts similarity index 96% rename from server/src/modules/playbooks-repository/PlaybooksRepositoryComponent.ts rename to server/src/modules/repository/PlaybooksRepositoryComponent.ts index 5c4048dd..7fe3fb53 100644 --- a/server/src/modules/playbooks-repository/PlaybooksRepositoryComponent.ts +++ b/server/src/modules/repository/PlaybooksRepositoryComponent.ts @@ -1,5 +1,7 @@ import pino from 'pino'; import shell from 'shelljs'; +import { SSM_DATA_PATH } from '../../config'; +import EventManager from '../../core/events/EventManager'; import { NotFoundError } from '../../middlewares/api/ApiError'; import Playbook from '../../data/database/model/Playbook'; import PlaybooksRepository from '../../data/database/model/PlaybooksRepository'; @@ -10,10 +12,10 @@ import { Playbooks } from '../../types/typings'; import Shell from '../shell'; import { recursivelyFlattenTree } from './tree-utils'; -export const DIRECTORY_ROOT = '/playbooks'; +export const DIRECTORY_ROOT = `${SSM_DATA_PATH}/playbooks`; export const FILE_PATTERN = /\.yml$/; -abstract class PlaybooksRepositoryComponent { +abstract class PlaybooksRepositoryComponent extends EventManager { public name: string; public directory: string; public uuid: string; @@ -21,6 +23,7 @@ abstract class PlaybooksRepositoryComponent { public rootPath: string; protected constructor(uuid: string, name: string, rootPath: string) { + super(); this.rootPath = rootPath; const dir = `${rootPath}/${uuid}`; this.uuid = uuid; diff --git a/server/src/modules/playbooks-repository/PlaybooksRepositoryEngine.ts b/server/src/modules/repository/PlaybooksRepositoryEngine.ts similarity index 87% rename from server/src/modules/playbooks-repository/PlaybooksRepositoryEngine.ts rename to server/src/modules/repository/PlaybooksRepositoryEngine.ts index ac52e8ae..75fa9383 100644 --- a/server/src/modules/playbooks-repository/PlaybooksRepositoryEngine.ts +++ b/server/src/modules/repository/PlaybooksRepositoryEngine.ts @@ -1,12 +1,12 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import PlaybooksRepository from '../../data/database/model/PlaybooksRepository'; import PlaybooksRepositoryRepo from '../../data/database/repository/PlaybooksRepositoryRepo'; import PinoLogger from '../../logger'; import { DEFAULT_VAULT_ID, vaultDecrypt } from '../ansible-vault/ansible-vault'; -import GitRepositoryComponent from './git-repository/GitRepositoryComponent'; -import LocalRepositoryComponent from './local-repository/LocalRepositoryComponent'; +import GitPlaybooksRepositoryComponent from './git-playbooks-repository/GitPlaybooksRepositoryComponent'; +import LocalPlaybooksRepositoryComponent from './local-playbooks-repository/LocalPlaybooksRepositoryComponent'; import { AbstractComponent } from './PlaybooksRepositoryComponent'; -import { saveSSMDefaultPlaybooksRepositories } from './default-repositories'; +import { saveSSMDefaultPlaybooksRepositories } from './default-playbooks-repositories'; const logger = PinoLogger.child( { module: 'PlaybooksRepositoryEngine' }, @@ -38,7 +38,7 @@ async function registerGitRepository(playbookRepository: PlaybooksRepository) { if (!decryptedAccessToken) { throw new Error('Error decrypting access token'); } - return new GitRepositoryComponent( + return new GitPlaybooksRepositoryComponent( uuid, logger, name, @@ -55,7 +55,7 @@ async function registerLocalRepository(playbookRepository: PlaybooksRepository) if (!playbookRepository.directory) { throw new Error('playbookRepository.directory is required'); } - return new LocalRepositoryComponent( + return new LocalPlaybooksRepositoryComponent( playbookRepository.uuid, logger, playbookRepository.name, @@ -69,11 +69,11 @@ async function registerRepository(playbookRepository: PlaybooksRepository) { ); switch (playbookRepository.type) { - case Playbooks.PlaybooksRepositoryType.GIT: + case Repositories.RepositoryType.GIT: state.playbooksRepository[playbookRepository.uuid] = await registerGitRepository(playbookRepository); break; - case Playbooks.PlaybooksRepositoryType.LOCAL: + case Repositories.RepositoryType.LOCAL: state.playbooksRepository[playbookRepository.uuid] = await registerLocalRepository(playbookRepository); break; diff --git a/server/src/modules/playbooks-repository/default-repositories.ts b/server/src/modules/repository/default-playbooks-repositories.ts similarity index 73% rename from server/src/modules/playbooks-repository/default-repositories.ts rename to server/src/modules/repository/default-playbooks-repositories.ts index b1e2843f..2eaa0c28 100644 --- a/server/src/modules/playbooks-repository/default-repositories.ts +++ b/server/src/modules/repository/default-playbooks-repositories.ts @@ -1,4 +1,5 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; +import { SSM_DATA_PATH, SSM_INSTALL_PATH } from '../../config'; import PlaybooksRepositoryRepo from '../../data/database/repository/PlaybooksRepositoryRepo'; import UserRepo from '../../data/database/repository/UserRepo'; import PinoLogger from '../../logger'; @@ -13,8 +14,8 @@ const corePlaybooksRepository = { name: 'ssm-core', uuid: '00000000-0000-0000-0000-000000000000', enabled: true, - type: Playbooks.PlaybooksRepositoryType.LOCAL, - directory: '/opt/squirrelserversmanager/server/src/ansible/00000000-0000-0000-0000-000000000000', + type: Repositories.RepositoryType.LOCAL, + directory: `${SSM_INSTALL_PATH}/server/src/ansible/00000000-0000-0000-0000-000000000000`, default: true, }; @@ -22,8 +23,8 @@ const toolsPlaybooksRepository = { name: 'ssm-tools', uuid: '00000000-0000-0000-0000-000000000001', enabled: true, - type: Playbooks.PlaybooksRepositoryType.LOCAL, - directory: '/opt/squirrelserversmanager/server/src/ansible/00000000-0000-0000-0000-000000000001', + type: Repositories.RepositoryType.LOCAL, + directory: `${SSM_INSTALL_PATH}/server/src/ansible/00000000-0000-0000-0000-000000000001`, default: true, }; @@ -38,8 +39,8 @@ export async function createADefaultLocalUserRepository() { const userPlaybooksRepository = { name: user?.email.trim().split('@')[0] || 'user-default', enabled: true, - type: Playbooks.PlaybooksRepositoryType.LOCAL, - directory: '/playbooks/00000000-0000-0000-0000-000000000002', + type: Repositories.RepositoryType.LOCAL, + directory: `${SSM_DATA_PATH}/playbooks/00000000-0000-0000-0000-000000000002`, uuid: '00000000-0000-0000-0000-000000000002', }; await PlaybooksRepositoryRepo.updateOrCreate(userPlaybooksRepository); diff --git a/server/src/modules/playbooks-repository/git-repository/GitRepositoryComponent.ts b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts similarity index 63% rename from server/src/modules/playbooks-repository/git-repository/GitRepositoryComponent.ts rename to server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts index 71d13c15..9a8e3970 100644 --- a/server/src/modules/playbooks-repository/git-repository/GitRepositoryComponent.ts +++ b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts @@ -1,3 +1,7 @@ +import { SsmAlert } from 'ssm-shared-lib'; +import Events from '../../../core/events/events'; +import logger from '../../../logger'; +import GitPlaybooksRepositoryUseCases from '../../../services/GitPlaybooksRepositoryUseCases'; import PlaybooksRepositoryComponent, { AbstractComponent, DIRECTORY_ROOT, @@ -13,7 +17,10 @@ import { forcePull, } from '../../../helpers/git'; -class GitRepositoryComponent extends PlaybooksRepositoryComponent implements AbstractComponent { +class GitPlaybooksRepositoryComponent + extends PlaybooksRepositoryComponent + implements AbstractComponent +{ private readonly options: IInitGitOptionsSyncImmediately; constructor( @@ -43,20 +50,24 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs }; this.childLogger = logger.child( - { module: `GitRepository`, moduleId: `${this.uuid}`, moduleName: `${this.name}` }, - { msgPrefix: `[GIT_REPOSITORY] - ` }, + { module: `PlaybooksGitRepository`, moduleId: `${this.uuid}`, moduleName: `${this.name}` }, + { msgPrefix: `[PLAYBOOKS_GIT_REPOSITORY] - ` }, ); } - async clone() { + async clone(syncAfter = false) { this.childLogger.info('Clone starting...'); try { - void Shell.FileSystemManager.createDirectory(this.directory, DIRECTORY_ROOT); + try { + void Shell.FileSystemManager.createDirectory(this.directory, DIRECTORY_ROOT); + } catch (error: any) { + logger.warn(error); + } await clone({ ...this.options, logger: { debug: (message: string, context: ILoggerContext): unknown => - this.childLogger.info(message, { callerFunction: 'clone', ...context }), + this.childLogger.debug(message, { callerFunction: 'clone', ...context }), warn: (message: string, context: ILoggerContext): unknown => this.childLogger.warn(message, { callerFunction: 'clone', ...context }), info: (message: GitStep, context: ILoggerContext): void => { @@ -67,8 +78,17 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs }, }, }); - } catch (error) { + if (syncAfter) { + await this.syncToDatabase(); + } + } catch (error: any) { this.childLogger.error(error); + await GitPlaybooksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during clone: ${error.message}`, + module: 'GitPlaybooksRepositoryComponent', + }); } } @@ -89,8 +109,14 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs }, }, }); - } catch (error) { + } catch (error: any) { this.childLogger.error(error); + await GitPlaybooksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during commit and sync: ${error.message}`, + module: 'GitPlaybooksRepositoryComponent', + }); } } @@ -111,8 +137,14 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs }, }, }); - } catch (error) { + } catch (error: any) { this.childLogger.error(error); + await GitPlaybooksRepositoryUseCases.putRepositoryOnError(this.uuid, error); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.ERROR, + message: `Error during forcePull: ${error.message}`, + module: 'GitPlaybooksRepositoryComponent', + }); } } @@ -125,4 +157,4 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs } } -export default GitRepositoryComponent; +export default GitPlaybooksRepositoryComponent; diff --git a/server/src/modules/playbooks-repository/local-repository/LocalRepositoryComponent.ts b/server/src/modules/repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent.ts similarity index 59% rename from server/src/modules/playbooks-repository/local-repository/LocalRepositoryComponent.ts rename to server/src/modules/repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent.ts index c6be3fc5..974dd9aa 100644 --- a/server/src/modules/playbooks-repository/local-repository/LocalRepositoryComponent.ts +++ b/server/src/modules/repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent.ts @@ -1,12 +1,15 @@ import PlaybooksRepositoryComponent, { AbstractComponent } from '../PlaybooksRepositoryComponent'; import Shell from '../../shell'; -class LocalRepositoryComponent extends PlaybooksRepositoryComponent implements AbstractComponent { +class LocalPlaybooksRepositoryComponent + extends PlaybooksRepositoryComponent + implements AbstractComponent +{ constructor(uuid: string, logger: any, name: string, rootPath: string) { super(uuid, name, rootPath); this.childLogger = logger.child( - { module: `LocalRepository`, moduleId: `${this.uuid}`, moduleName: `${this.name}` }, - { msgPrefix: `[LOCAL_REPOSITORY] - ` }, + { module: `PlaybooksLocalRepository`, moduleId: `${this.uuid}`, moduleName: `${this.name}` }, + { msgPrefix: `[PLAYBOOKS_LOCAL_REPOSITORY] - ` }, ); } @@ -19,4 +22,4 @@ class LocalRepositoryComponent extends PlaybooksRepositoryComponent implements A } } -export default LocalRepositoryComponent; +export default LocalPlaybooksRepositoryComponent; diff --git a/server/src/modules/playbooks-repository/tree-utils.ts b/server/src/modules/repository/tree-utils.ts similarity index 100% rename from server/src/modules/playbooks-repository/tree-utils.ts rename to server/src/modules/repository/tree-utils.ts diff --git a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts index 4baeb3df..85ee34f6 100644 --- a/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts +++ b/server/src/modules/shell/managers/AnsibleShellCommandsManager.ts @@ -1,6 +1,7 @@ import shell from 'shelljs'; import { API, SsmAnsible } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; +import { SSM_INSTALL_PATH } from '../../../config'; import User from '../../../data/database/model/User'; import AnsibleTaskRepo from '../../../data/database/repository/AnsibleTaskRepo'; import DeviceAuthRepo from '../../../data/database/repository/DeviceAuthRepo'; @@ -18,7 +19,7 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { 'Ansible', ); } - private readonly ANSIBLE_PATH = '/opt/squirrelserversmanager/server/src/ansible/'; + private readonly ANSIBLE_PATH = `${SSM_INSTALL_PATH}/server/src/ansible/`; static timeout(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -62,8 +63,8 @@ class AnsibleShellCommandsManager extends AbstractShellCommander { mode: SsmAnsible.ExecutionMode = SsmAnsible.ExecutionMode.APPLY, ) { shell.cd(this.ANSIBLE_PATH); - shell.rm('/opt/squirrelserversmanager/server/src/playbooks/inventory/hosts'); - shell.rm('/opt/squirrelserversmanager/server/src/playbooks/env/_extravars'); + shell.rm(`${SSM_INSTALL_PATH}/server/src/playbooks/inventory/hosts`); + shell.rm(`${SSM_INSTALL_PATH}/server/src/playbooks/env/_extravars`); const uuid = uuidv4(); const result = await new Promise((resolve) => { const cmd = ansibleCmd.buildAnsibleCmd( diff --git a/server/src/modules/shell/managers/FileSystemManager.ts b/server/src/modules/shell/managers/FileSystemManager.ts index d583c27e..67cd82c4 100644 --- a/server/src/modules/shell/managers/FileSystemManager.ts +++ b/server/src/modules/shell/managers/FileSystemManager.ts @@ -33,6 +33,10 @@ class FileSystemManager extends AbstractShellCommander { return this.executeCommand(shellWrapper.test, options, path); } + readFile(path: string): string { + return this.executeCommand(shellWrapper.cat, path).toString(); + } + protected checkPath(userPath: string, rootPath?: string) { if (rootPath) { const filePath = path.resolve(rootPath, userPath); diff --git a/server/src/routes/container-stacks-repository.ts b/server/src/routes/container-stacks-repository.ts new file mode 100644 index 00000000..2f7c6e8f --- /dev/null +++ b/server/src/routes/container-stacks-repository.ts @@ -0,0 +1,47 @@ +import express from 'express'; +import passport from 'passport'; +import { + addGitRepository, + commitAndSyncRepository, + deleteGitRepository, + forceCloneRepository, + forcePullRepository, + forceRegister, + getGitRepositories, + syncToDatabaseRepository, + updateGitRepository, +} from '../controllers/rest/containers-stacks-repository/git'; +import { + addGitRepositoryValidator, + genericGitRepositoryActionValidator, + updateGitRepositoryValidator, +} from '../controllers/rest/containers-stacks-repository/git.validator'; + +const router = express.Router(); + +router.use(passport.authenticate('jwt', { session: false })); + +router + .route('/git/') + .get(getGitRepositories) + .put(addGitRepositoryValidator, addGitRepository) + .put(addGitRepositoryValidator, addGitRepository); +router + .route('/git/:uuid') + .post(updateGitRepositoryValidator, updateGitRepository) + .delete(genericGitRepositoryActionValidator, deleteGitRepository); +router + .route('/git/:uuid/sync-to-database-repository') + .post(genericGitRepositoryActionValidator, syncToDatabaseRepository); +router + .route('/git/:uuid/force-pull-repository') + .post(genericGitRepositoryActionValidator, forcePullRepository); +router + .route('/git/:uuid/force-clone-repository') + .post(genericGitRepositoryActionValidator, forceCloneRepository); +router + .route('/git/:uuid/commit-and-sync-repository') + .post(genericGitRepositoryActionValidator, commitAndSyncRepository); +router.route('/git/:uuid/force-register').post(genericGitRepositoryActionValidator, forceRegister); + +export default router; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 0dc7e8a6..a0b5cb41 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -11,6 +11,7 @@ import playbooks from './playbooks'; import playbooksRepository from './playbooks-repository'; import settings from './settings'; import user from './user'; +import containerRepository from './container-stacks-repository'; const router = express.Router(); @@ -27,5 +28,6 @@ router.use('/playbooks-repository', playbooksRepository); router.use('/automations', automations); router.use('/notifications', notifications); router.use('/ansible', ansible); +router.use('/container-repository', containerRepository); export default router; diff --git a/server/src/services/GitCustomStacksRepositoryUseCases.ts b/server/src/services/GitCustomStacksRepositoryUseCases.ts new file mode 100644 index 00000000..28d2b866 --- /dev/null +++ b/server/src/services/GitCustomStacksRepositoryUseCases.ts @@ -0,0 +1,118 @@ +import { v4 as uuidv4 } from 'uuid'; +import ContainerCustomStackRepository from '../data/database/model/ContainerCustomStackRepository'; +import ContainerCustomStackRepositoryRepo from '../data/database/repository/ContainerCustomStackRepositoryRepo'; +import { InternalError } from '../middlewares/api/ApiError'; +import ContainerCustomStacksRepositoryComponent from '../modules/repository/ContainerCustomStacksRepositoryComponent'; +import ContainerCustomStacksRepositoryEngine from '../modules/repository/ContainerCustomStacksRepositoryEngine'; +import Shell from '../modules/shell'; + +async function addGitRepository( + name: string, + accessToken: string, + branch: string, + email: string, + userName: string, + remoteUrl: string, + matchesList?: string[], +) { + const uuid = uuidv4(); + const gitRepository = await ContainerCustomStacksRepositoryEngine.registerRepository({ + uuid, + name, + branch, + email, + userName, + accessToken, + remoteUrl, + enabled: true, + matchesList, + }); + await ContainerCustomStackRepositoryRepo.create({ + uuid, + name, + remoteUrl, + accessToken, + branch, + email, + userName, + enabled: true, + matchesList, + }); + void gitRepository.clone(true); +} + +async function updateGitRepository( + uuid: string, + name: string, + accessToken: string, + branch: string, + email: string, + userName: string, + remoteUrl: string, + matchesList?: string[], +) { + await ContainerCustomStacksRepositoryEngine.deregisterRepository(uuid); + await ContainerCustomStacksRepositoryEngine.registerRepository({ + uuid, + name, + branch, + email, + userName, + accessToken, + remoteUrl, + enabled: true, + matchesList, + }); + await ContainerCustomStackRepositoryRepo.update({ + uuid, + name, + remoteUrl, + accessToken, + branch, + email, + userName, + enabled: true, + matchesList, + }); +} + +async function deleteRepository(repository: ContainerCustomStackRepository): Promise { + const repositoryComponent = ContainerCustomStacksRepositoryEngine.getState().stackRepository[ + repository.uuid + ] as ContainerCustomStacksRepositoryComponent; + if (!repositoryComponent) { + throw new InternalError(`Container Custom Stacks Repository doesnt seem registered`); + } + const directory = repositoryComponent.getDirectory(); + await ContainerCustomStacksRepositoryEngine.deregisterRepository(repository.uuid); + await ContainerCustomStackRepositoryRepo.deleteByUuid(repository.uuid); + Shell.FileSystemManager.deleteFiles(directory); +} + +async function putRepositoryOnError(repositoryUuid: string, error: any) { + const repository = await ContainerCustomStackRepositoryRepo.findOneByUuid(repositoryUuid); + if (!repository) { + throw new Error(`Repository with Uuid: ${repositoryUuid} not found`); + } + repository.onError = true; + repository.onErrorMessage = `${error.message}`; + await ContainerCustomStackRepositoryRepo.update(repository); +} + +async function resetRepositoryError(repositoryUuid: string) { + const repository = await ContainerCustomStackRepositoryRepo.findOneByUuid(repositoryUuid); + if (!repository) { + throw new Error(`Repository with Uuid: ${repositoryUuid} not found`); + } + repository.onError = false; + repository.onErrorMessage = undefined; + await ContainerCustomStackRepositoryRepo.update(repository); +} + +export default { + addGitRepository, + updateGitRepository, + deleteRepository, + putRepositoryOnError, + resetRepositoryError, +}; diff --git a/server/src/services/GitRepositoryUseCases.ts b/server/src/services/GitPlaybooksRepositoryUseCases.ts similarity index 57% rename from server/src/services/GitRepositoryUseCases.ts rename to server/src/services/GitPlaybooksRepositoryUseCases.ts index 0c69b533..cf2f71b4 100644 --- a/server/src/services/GitRepositoryUseCases.ts +++ b/server/src/services/GitPlaybooksRepositoryUseCases.ts @@ -1,7 +1,7 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; -import PlaybooksRepositoryEngine from '../modules/playbooks-repository/PlaybooksRepositoryEngine'; +import PlaybooksRepositoryEngine from '../modules/repository/PlaybooksRepositoryEngine'; async function addGitRepository( name: string, @@ -15,7 +15,7 @@ async function addGitRepository( const uuid = uuidv4(); const gitRepository = await PlaybooksRepositoryEngine.registerRepository({ uuid, - type: Playbooks.PlaybooksRepositoryType.GIT, + type: Repositories.RepositoryType.GIT, name, branch, email, @@ -27,7 +27,7 @@ async function addGitRepository( }); await PlaybooksRepositoryRepo.create({ uuid, - type: Playbooks.PlaybooksRepositoryType.GIT, + type: Repositories.RepositoryType.GIT, name, remoteUrl, accessToken, @@ -38,8 +38,7 @@ async function addGitRepository( enabled: true, directoryExclusionList, }); - void gitRepository.clone(); - void gitRepository.syncToDatabase(); + void gitRepository.clone(true); } async function updateGitRepository( @@ -55,7 +54,7 @@ async function updateGitRepository( await PlaybooksRepositoryEngine.deregisterRepository(uuid); const gitRepository = await PlaybooksRepositoryEngine.registerRepository({ uuid, - type: Playbooks.PlaybooksRepositoryType.GIT, + type: Repositories.RepositoryType.GIT, name, branch, email, @@ -67,7 +66,7 @@ async function updateGitRepository( }); await PlaybooksRepositoryRepo.update({ uuid, - type: Playbooks.PlaybooksRepositoryType.GIT, + type: Repositories.RepositoryType.GIT, name, remoteUrl, accessToken, @@ -80,7 +79,29 @@ async function updateGitRepository( }); } +async function putRepositoryOnError(repositoryUuid: string, error: any) { + const repository = await PlaybooksRepositoryRepo.findByUuid(repositoryUuid); + if (!repository) { + throw new Error(`Repository with Uuid: ${repositoryUuid} not found`); + } + repository.onError = true; + repository.onErrorMessage = `${error.message}`; + await PlaybooksRepositoryRepo.update(repository); +} + +async function resetRepositoryError(repositoryUuid: string) { + const repository = await PlaybooksRepositoryRepo.findByUuid(repositoryUuid); + if (!repository) { + throw new Error(`Repository with Uuid: ${repositoryUuid} not found`); + } + repository.onError = false; + repository.onErrorMessage = undefined; + await PlaybooksRepositoryRepo.update(repository); +} + export default { addGitRepository, updateGitRepository, + putRepositoryOnError, + resetRepositoryError, }; diff --git a/server/src/services/LocalRepositoryUseCases.ts b/server/src/services/LocalPlaybooksRepositoryUseCases.ts similarity index 82% rename from server/src/services/LocalRepositoryUseCases.ts rename to server/src/services/LocalPlaybooksRepositoryUseCases.ts index cf679e4f..d4935043 100644 --- a/server/src/services/LocalRepositoryUseCases.ts +++ b/server/src/services/LocalPlaybooksRepositoryUseCases.ts @@ -1,10 +1,10 @@ -import { Playbooks } from 'ssm-shared-lib'; +import { Repositories } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; import PinoLogger from '../logger'; import { NotFoundError } from '../middlewares/api/ApiError'; -import { DIRECTORY_ROOT } from '../modules/playbooks-repository/PlaybooksRepositoryComponent'; -import PlaybooksRepositoryEngine from '../modules/playbooks-repository/PlaybooksRepositoryEngine'; +import { DIRECTORY_ROOT } from '../modules/repository/PlaybooksRepositoryComponent'; +import PlaybooksRepositoryEngine from '../modules/repository/PlaybooksRepositoryEngine'; const logger = PinoLogger.child( { module: 'LocalRepositoryUseCases' }, @@ -15,7 +15,7 @@ async function addLocalRepository(name: string, directoryExclusionList?: string[ const uuid = uuidv4(); const localRepository = await PlaybooksRepositoryEngine.registerRepository({ uuid, - type: Playbooks.PlaybooksRepositoryType.LOCAL, + type: Repositories.RepositoryType.LOCAL, name, enabled: true, directory: DIRECTORY_ROOT, @@ -23,7 +23,7 @@ async function addLocalRepository(name: string, directoryExclusionList?: string[ }); await PlaybooksRepositoryRepo.create({ uuid, - type: Playbooks.PlaybooksRepositoryType.LOCAL, + type: Repositories.RepositoryType.LOCAL, name, directory: localRepository.getDirectory(), enabled: true, diff --git a/server/src/services/PlaybooksRepositoryUseCases.ts b/server/src/services/PlaybooksRepositoryUseCases.ts index c62c40a2..a2a6499e 100644 --- a/server/src/services/PlaybooksRepositoryUseCases.ts +++ b/server/src/services/PlaybooksRepositoryUseCases.ts @@ -4,9 +4,9 @@ import Playbook, { PlaybookModel } from '../data/database/model/Playbook'; import PlaybooksRepository from '../data/database/model/PlaybooksRepository'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; import PinoLogger from '../logger'; -import PlaybooksRepositoryComponent from '../modules/playbooks-repository/PlaybooksRepositoryComponent'; -import PlaybooksRepositoryEngine from '../modules/playbooks-repository/PlaybooksRepositoryEngine'; -import { recursiveTreeCompletion } from '../modules/playbooks-repository/tree-utils'; +import PlaybooksRepositoryComponent from '../modules/repository/PlaybooksRepositoryComponent'; +import PlaybooksRepositoryEngine from '../modules/repository/PlaybooksRepositoryEngine'; +import { recursiveTreeCompletion } from '../modules/repository/tree-utils'; import Shell from '../modules/shell'; const logger = PinoLogger.child( diff --git a/server/src/tests/unit-tests/helpers/ansible/AnsibleConfigurationHelper.test.ts b/server/src/tests/unit-tests/helpers/ansible/AnsibleConfigurationHelper.test.ts index 6948ea57..d1b4767a 100644 --- a/server/src/tests/unit-tests/helpers/ansible/AnsibleConfigurationHelper.test.ts +++ b/server/src/tests/unit-tests/helpers/ansible/AnsibleConfigurationHelper.test.ts @@ -4,7 +4,7 @@ import { readConfig, writeConfig } from '../../../../helpers/ansible/AnsibleConf vi.mock('fs'); -const CONFIG_FILE = '/ansible-config/ansible.cfg'; +const CONFIG_FILE = '/data/config/ansible.cfg'; const mockFsReadFileSync = vi.spyOn(fs, 'readFileSync'); const mockFsWriteFileSync = vi.spyOn(fs, 'writeFileSync'); diff --git a/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts b/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts index 74adc5ed..c154bf1d 100644 --- a/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts +++ b/server/src/tests/unit-tests/modules/ansible/AnsibleCmd.test.ts @@ -100,7 +100,7 @@ describe('buildAnsibleCmd() function', () => { const result = AnsibleCmd.buildAnsibleCmd(playbook, uuid, inventory, user, extraVars); const expectedStart = - "sudo SSM_API_KEY=testKey ANSIBLE_CONFIG=/ansible-config/ansible.cfg ANSIBLE_FORCE_COLOR=1 python3 ssm-ansible-run.py --playbook 'testPlaybook' --ident 'testUuid'"; + "sudo SSM_API_KEY=testKey ANSIBLE_CONFIG=/data/config/ansible.cfg ANSIBLE_FORCE_COLOR=1 python3 ssm-ansible-run.py --playbook 'testPlaybook' --ident 'testUuid'"; expect(result.startsWith(expectedStart)).toEqual(true); }); diff --git a/server/src/tests/unit-tests/modules/playbooks-repository/PlaybookRepositoryComponent.test.ts b/server/src/tests/unit-tests/modules/playbooks-repository/PlaybookRepositoryComponent.test.ts index e37ea96e..f5d523ca 100644 --- a/server/src/tests/unit-tests/modules/playbooks-repository/PlaybookRepositoryComponent.test.ts +++ b/server/src/tests/unit-tests/modules/playbooks-repository/PlaybookRepositoryComponent.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -import LocalRepositoryComponent from '../../../../modules/playbooks-repository/local-repository/LocalRepositoryComponent'; -import PlaybooksRepositoryComponent from '../../../../modules/playbooks-repository/PlaybooksRepositoryComponent'; +import LocalPlaybooksRepositoryComponent from '../../../../modules/repository/local-playbooks-repository/LocalPlaybooksRepositoryComponent'; +import PlaybooksRepositoryComponent from '../../../../modules/repository/PlaybooksRepositoryComponent'; describe('PlaybooksRepositoryComponent', () => { let playbooksRepositoryComponent: PlaybooksRepositoryComponent; @@ -11,7 +11,12 @@ describe('PlaybooksRepositoryComponent', () => { return { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; }, }; - playbooksRepositoryComponent = new LocalRepositoryComponent('uuid', logger, 'name', 'path'); + playbooksRepositoryComponent = new LocalPlaybooksRepositoryComponent( + 'uuid', + logger, + 'name', + 'path', + ); }); describe('fileBelongToRepository method', () => { diff --git a/server/src/tests/unit-tests/modules/playbooks-repository/tree-utils.test.ts b/server/src/tests/unit-tests/modules/playbooks-repository/tree-utils.test.ts index 05a2beaa..946f62ae 100644 --- a/server/src/tests/unit-tests/modules/playbooks-repository/tree-utils.test.ts +++ b/server/src/tests/unit-tests/modules/playbooks-repository/tree-utils.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from 'vitest'; import { recursiveTreeCompletion, recursivelyFlattenTree, -} from '../../../../modules/playbooks-repository/tree-utils'; +} from '../../../../modules/repository/tree-utils'; const mockTree: DirectoryTree.TreeNode = { path: '/root', diff --git a/shared-lib/src/enums/alert.ts b/shared-lib/src/enums/alert.ts new file mode 100644 index 00000000..66160b7a --- /dev/null +++ b/shared-lib/src/enums/alert.ts @@ -0,0 +1,3 @@ +export enum AlertType { + ERROR = 'error' +} diff --git a/shared-lib/src/enums/playbooks.ts b/shared-lib/src/enums/playbooks.ts deleted file mode 100644 index e24a5a11..00000000 --- a/shared-lib/src/enums/playbooks.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum PlaybooksRepositoryType { - LOCAL = 'local', - GIT = 'git', -} diff --git a/shared-lib/src/enums/repositories.ts b/shared-lib/src/enums/repositories.ts new file mode 100644 index 00000000..ecae67b3 --- /dev/null +++ b/shared-lib/src/enums/repositories.ts @@ -0,0 +1,4 @@ +export enum RepositoryType { + LOCAL = 'local', + GIT = 'git', +} diff --git a/shared-lib/src/index.ts b/shared-lib/src/index.ts index 097da68f..6cd6a171 100644 --- a/shared-lib/src/index.ts +++ b/shared-lib/src/index.ts @@ -6,8 +6,9 @@ export * as SsmAnsible from './enums/ansible'; export * as Validation from './validation/index'; export * as StatsType from './enums/stats'; export * as DirectoryTree from './types/tree' -export * as Playbooks from './enums/playbooks' +export * as Repositories from './enums/repositories' export * as SsmContainer from './enums/container' export * as Automations from './form/automation'; export * as SsmEvents from './types/events'; export * as SsmAgent from './enums/agent'; +export * as SsmAlert from './enums/alert'; diff --git a/shared-lib/src/types/api.ts b/shared-lib/src/types/api.ts index a2b0af10..b88232fd 100644 --- a/shared-lib/src/types/api.ts +++ b/shared-lib/src/types/api.ts @@ -1,5 +1,5 @@ import { ExtraVarsType, SSHConnection, SSHType } from '../enums/ansible'; -import { PlaybooksRepositoryType } from '../enums/playbooks'; +import { RepositoryType } from '../enums/repositories'; import { AutomationChain } from '../form/automation'; import { ExtendedTreeNode } from './tree'; @@ -350,7 +350,7 @@ export type PlaybooksRepository = { name: string; uuid: string; path: string; - type: PlaybooksRepositoryType; + type: RepositoryType; children?: ExtendedTreeNode[]; directoryExclusionList?: string[]; }; @@ -648,7 +648,7 @@ export type ContainerRegistry = { canAnonymous: boolean; } -export type GitRepository = PlaybooksRepository & { +export type GitPlaybooksRepository = PlaybooksRepository & { email: string; branch: string; userName: string; @@ -656,12 +656,25 @@ export type GitRepository = PlaybooksRepository & { default: boolean; } -export type LocalRepository = PlaybooksRepository & { +export type LocalPlaybooksRepository = PlaybooksRepository & { directory: string; enabled: boolean; default: boolean; } +export type GitContainerStacksRepository = { + email: string; + branch: string; + userName: string; + remoteUrl: string; + default: boolean; + name: string; + uuid: string; + matchesList?: string[]; + onError?: boolean; + onErrorMessage?: string; +} + export type ExtraVars = ExtraVar[]; export type Automation = { diff --git a/shared-lib/src/types/events.ts b/shared-lib/src/types/events.ts index 56202c39..149b08bb 100644 --- a/shared-lib/src/types/events.ts +++ b/shared-lib/src/types/events.ts @@ -22,3 +22,7 @@ export enum Common { DISCONNECT = 'disconnect', ERROR = 'error' } + +export enum Alert { + NEW_ALERT = 'alert:new', +}