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={
+
+ }
+ onClick={() => {
+ setSelectedGitRecord(undefined);
+ setGitModalOpened(true);
+ }}
+ >
+ Add a new remote repository
+
+
+
+
+
+ }
+ >
+
+ 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',
+}