diff --git a/client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx index 62db352d..7458902b 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/forms/DirectoryExclusionForm.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { API } from 'ssm-shared-lib'; type DirectoryExclusionFormProps = { - selectedRecord: Partial; + selectedRecord: Partial; }; const DirectoryExclusionForm: React.FC = ( diff --git a/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx b/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx index 9243ed3a..aae9461b 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/forms/GitForm.tsx @@ -1,6 +1,11 @@ -import { ProForm, ProFormText } from '@ant-design/pro-components'; +import { capitalizeFirstLetter } from '@/utils/strings'; +import { + ProForm, + ProFormSelect, + ProFormText, +} from '@ant-design/pro-components'; import React from 'react'; -import { API } from 'ssm-shared-lib'; +import { API, SsmGit } from 'ssm-shared-lib'; export type GitFormProps = { selectedRecord: Partial< @@ -33,6 +38,17 @@ const GitForm: React.FC = ({ selectedRecord, repositories }) => ( }, ]} /> + ({ + label: capitalizeFirstLetter(e), + value: e, + }))} + rules={[{ required: true }]} + initialValue={selectedRecord?.gitService || SsmGit.Services.Github} + /> { userName, remoteUrl, matchesList, - }: { - name: string; - accessToken: string; - branch: string; - email: string; - userName: string; - remoteUrl: string; - matchesList?: string[]; - } = req.body; + gitService, + }: API.GitContainerStacksRepository = req.body; await GitRepositoryUseCases.addGitRepository( name, - await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + await vaultEncrypt(accessToken as string, DEFAULT_VAULT_ID), branch, email, userName, remoteUrl, + gitService, matchesList, ); new SuccessResponse('Added container stacks git repository').send(res); @@ -52,26 +47,20 @@ export const updateGitRepository = async (req, res) => { accessToken, branch, email, - gitUserName, + userName, remoteUrl, matchesList, - }: { - name: string; - accessToken: string; - branch: string; - email: string; - gitUserName: string; - remoteUrl: string; - matchesList?: string[]; - } = req.body; + gitService, + }: API.GitContainerStacksRepository = req.body; await GitRepositoryUseCases.updateGitRepository( uuid, name, - await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + await vaultEncrypt(accessToken as string, DEFAULT_VAULT_ID), branch, email, - gitUserName, + userName, remoteUrl, + gitService, matchesList, ); new SuccessResponse('Updated container 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 index 00efc0fc..9fabaaee 100644 --- a/server/src/controllers/rest/containers-stacks-repository/git.validator.ts +++ b/server/src/controllers/rest/containers-stacks-repository/git.validator.ts @@ -1,4 +1,5 @@ import { body, param } from 'express-validator'; +import { SsmGit } from 'ssm-shared-lib'; import validator from '../../../middlewares/Validator'; export const addGitRepositoryValidator = [ @@ -9,6 +10,10 @@ export const addGitRepositoryValidator = [ body('userName').exists().isString().withMessage('userName is incorrect'), body('remoteUrl').exists().isURL().withMessage('remoteUrl is incorrect'), body('matchesList').exists().isArray().withMessage('matchesList is incorrect'), + body('gitService') + .exists() + .isIn(Object.values(SsmGit.Services)) + .withMessage('Git service is required'), validator, ]; @@ -21,6 +26,10 @@ export const updateGitRepositoryValidator = [ body('userName').exists().isString().withMessage('userName is incorrect'), body('remoteUrl').exists().isURL().withMessage('remoteUrl is incorrect'), body('matchesList').exists().isArray().withMessage('matchesListis incorrect'), + body('gitService') + .exists() + .isIn(Object.values(SsmGit.Services)) + .withMessage('Git service is required'), validator, ]; diff --git a/server/src/controllers/rest/playbooks-repository/git.ts b/server/src/controllers/rest/playbooks-repository/git.ts index 5e2418a0..42449f34 100644 --- a/server/src/controllers/rest/playbooks-repository/git.ts +++ b/server/src/controllers/rest/playbooks-repository/git.ts @@ -1,4 +1,4 @@ -import { Repositories } from 'ssm-shared-lib'; +import { API, Repositories } from 'ssm-shared-lib'; import PlaybooksRepositoryRepo from '../../../data/database/repository/PlaybooksRepositoryRepo'; import { NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; @@ -17,22 +17,16 @@ export const addGitRepository = async (req, res) => { userName, remoteUrl, directoryExclusionList, - }: { - name: string; - accessToken: string; - branch: string; - email: string; - userName: string; - remoteUrl: string; - directoryExclusionList?: string[]; - } = req.body; + gitService, + }: API.GitPlaybooksRepository = req.body; await GitRepositoryUseCases.addGitRepository( name, - await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + await vaultEncrypt(accessToken as string, DEFAULT_VAULT_ID), branch, email, userName, remoteUrl, + gitService, directoryExclusionList, ); new SuccessResponse('Added playbooks git repository').send(res); @@ -56,27 +50,21 @@ export const updateGitRepository = async (req, res) => { accessToken, branch, email, - gitUserName, + userName, remoteUrl, directoryExclusionList, - }: { - name: string; - accessToken: string; - branch: string; - email: string; - gitUserName: string; - remoteUrl: string; - directoryExclusionList?: string[]; - } = req.body; + gitService, + }: API.GitPlaybooksRepository = req.body; await GitRepositoryUseCases.updateGitRepository( uuid, name, - await vaultEncrypt(accessToken, DEFAULT_VAULT_ID), + await vaultEncrypt(accessToken as string, DEFAULT_VAULT_ID), branch, email, - gitUserName, + userName, remoteUrl, + gitService, directoryExclusionList, ); new SuccessResponse('Updated playbooks git repository').send(res); diff --git a/server/src/controllers/rest/playbooks-repository/git.validator.ts b/server/src/controllers/rest/playbooks-repository/git.validator.ts index d3fb9d4c..07cbacf4 100644 --- a/server/src/controllers/rest/playbooks-repository/git.validator.ts +++ b/server/src/controllers/rest/playbooks-repository/git.validator.ts @@ -1,4 +1,5 @@ import { body, param } from 'express-validator'; +import { SsmGit } from 'ssm-shared-lib'; import validator from '../../../middlewares/Validator'; export const addGitRepositoryValidator = [ @@ -12,6 +13,10 @@ export const addGitRepositoryValidator = [ .optional() .isArray() .withMessage('Directory exclusion list is incorrect'), + body('gitService') + .exists() + .isIn(Object.values(SsmGit.Services)) + .withMessage('Git service is required'), validator, ]; @@ -27,6 +32,10 @@ export const updateGitRepositoryValidator = [ .optional() .isArray() .withMessage('Directory exclusion list is incorrect'), + body('gitService') + .exists() + .isIn(Object.values(SsmGit.Services)) + .withMessage('Git service is required'), validator, ]; diff --git a/server/src/core/startup/index.ts b/server/src/core/startup/index.ts index 7c67a811..55e0f1a7 100644 --- a/server/src/core/startup/index.ts +++ b/server/src/core/startup/index.ts @@ -1,11 +1,13 @@ -import { Repositories, SettingsKeys } from 'ssm-shared-lib'; +import { Repositories, SettingsKeys, SsmGit } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import { getFromCache, setToCache } from '../../data/cache'; import initRedisValues from '../../data/cache/defaults'; import { ContainerCustomStackModel } from '../../data/database/model/ContainerCustomStack'; +import { ContainerCustomStacksRepositoryModel } from '../../data/database/model/ContainerCustomStackRepository'; import { ContainerVolumeModel } from '../../data/database/model/ContainerVolume'; import { DeviceModel } from '../../data/database/model/Device'; import { PlaybookModel } from '../../data/database/model/Playbook'; +import { PlaybooksRepositoryModel } from '../../data/database/model/PlaybooksRepository'; import { copyAnsibleCfgFileIfDoesntExist } from '../../helpers/ansible/AnsibleConfigurationHelper'; import PinoLogger from '../../logger'; import AutomationEngine from '../../modules/automations/AutomationEngine'; @@ -27,10 +29,10 @@ class Startup { async init() { this.logger.info(`Initializing...`); const schemeVersion = await this.initializeSchemeVersion(); - await this.initializeModules(); if (this.isSchemeVersionDifferent(schemeVersion)) { await this.updateScheme(); } + await this.initializeModules(); } private async initializeSchemeVersion(): Promise { @@ -51,27 +53,122 @@ class Startup { } private async updateScheme() { - this.logger.warn(`updateScheme- Scheme version differed, starting applying updates...`); - await PlaybookModel.syncIndexes(); - await DeviceModel.syncIndexes(); - await createADefaultLocalUserRepository(); - await initRedisValues(); - void setAnsibleVersions(); - await PlaybooksRepositoryEngine.syncAllRegistered(); - this.registerPersistedProviders(); - copyAnsibleCfgFileIfDoesntExist(); - const masterNodeUrl = await getFromCache('_ssm_masterNodeUrl'); - if (!masterNodeUrl) { - await setToCache('_ssm_masterNodeUrl', (await getFromCache('ansible-master-node-url')) || ''); + this.logger.warn('updateScheme - Scheme version differed, starting applying updates...'); + try { + await PlaybookModel.syncIndexes(); + this.logger.info('PlaybookModel indexes synchronized successfully.'); + } catch (error: any) { + this.logger.error(`Error synchronizing PlaybookModel indexes: ${error.message}`); + } + + try { + await DeviceModel.syncIndexes(); + this.logger.info('DeviceModel indexes synchronized successfully.'); + } catch (error: any) { + this.logger.error(`Error synchronizing DeviceModel indexes: ${error.message}`); + } + + try { + await createADefaultLocalUserRepository(); + this.logger.info('Created default local user repository successfully.'); + } catch (error: any) { + this.logger.error(`Error creating default local user repository: ${error.message}`); + } + + try { + await initRedisValues(); + this.logger.info('Initialized Redis values successfully.'); + } catch (error: any) { + this.logger.error(`Error initializing Redis values: ${error.message}`); + } + + try { + void setAnsibleVersions(); // Setting versions asynchronously without waiting + this.logger.info('Ansible versions set successfully.'); + } catch (error: any) { + this.logger.error(`Error setting Ansible versions: ${error.message}`); + } + + try { + await PlaybooksRepositoryEngine.syncAllRegistered(); + this.logger.info('All registered playbooks synced successfully.'); + } catch (error: any) { + this.logger.error(`Error syncing all registered playbooks: ${error.message}`); + } + + try { + this.registerPersistedProviders(); + this.logger.info('Persisted providers registered successfully.'); + } catch (error: any) { + this.logger.error(`Error registering persisted providers: ${error.message}`); } - await ContainerCustomStackModel.updateMany( - { type: { $exists: false } }, - { $set: { type: Repositories.RepositoryType.LOCAL } }, - ); - const containerVolumes = await ContainerVolumeModel.find({ uuid: { $exists: false } }); - for (const volume of containerVolumes) { - volume.uuid = uuidv4(); - await volume.save(); + + try { + copyAnsibleCfgFileIfDoesntExist(); + this.logger.info("Ansible configuration file copied if it didn't exist."); + } catch (error: any) { + this.logger.error(`Error copying Ansible configuration file: ${error.message}`); + } + + try { + const masterNodeUrl = await getFromCache('_ssm_masterNodeUrl'); + if (!masterNodeUrl) { + await setToCache( + '_ssm_masterNodeUrl', + (await getFromCache('ansible-master-node-url')) || '', + ); + this.logger.info('Master Node URL set in cache successfully.'); + } + } catch (error: any) { + this.logger.error(`Error managing master node URL in cache: ${error.message}`); + } + + try { + await ContainerCustomStackModel.updateMany( + { type: { $exists: false } }, + { $set: { type: Repositories.RepositoryType.LOCAL } }, + ); + this.logger.info('Container custom stack models updated successfully.'); + } catch (error: any) { + this.logger.error(`Error updating container custom stack models: ${error.message}`); + } + + try { + const containerVolumes = await ContainerVolumeModel.find({ uuid: { $exists: false } }); + for (const volume of containerVolumes) { + volume.uuid = uuidv4(); + await volume.save(); + } + this.logger.info('Container volumes updated successfully.'); + } catch (error: any) { + this.logger.error(`Error updating container volumes: ${error.message}`); + } + + try { + const containerCustomStackRepositories = await ContainerCustomStacksRepositoryModel.find({ + gitService: { $exists: false }, + }); + for (const repo of containerCustomStackRepositories) { + repo.gitService = SsmGit.Services.Github; + await repo.save(); + } + this.logger.info('Container custom stack repositories updated successfully.'); + } catch (error: any) { + this.logger.error(`Error updating container custom stack repositories: ${error.message}`); + } + + try { + const playbookGitRepositories = await PlaybooksRepositoryModel.find({ + gitService: { $exists: false }, + type: Repositories.RepositoryType.GIT, + }); + for (const repo of playbookGitRepositories) { + repo.gitService = SsmGit.Services.Github; + await repo.save(); + } + this.logger.info('Playbook Git repositories updated successfully.'); + } catch (error: any) { + this.logger.error(`Error updating playbook Git repositories: ${error.message}`); } } diff --git a/server/src/data/database/model/ContainerCustomStackRepository.ts b/server/src/data/database/model/ContainerCustomStackRepository.ts index ecd5cd82..07ada68d 100644 --- a/server/src/data/database/model/ContainerCustomStackRepository.ts +++ b/server/src/data/database/model/ContainerCustomStackRepository.ts @@ -1,4 +1,5 @@ import { Schema, model } from 'mongoose'; +import { SsmGit } from 'ssm-shared-lib'; export const DOCUMENT_NAME = 'ContainerCustomStackRepository'; export const COLLECTION_NAME = 'containercustomstackssrepository'; @@ -18,6 +19,7 @@ export default interface ContainerCustomStackRepository { updatedAt?: Date; onError?: boolean; onErrorMessage?: string; + gitService: SsmGit.Services; } const schema = new Schema( @@ -67,6 +69,10 @@ const schema = new Schema( type: Schema.Types.String, required: false, }, + gitService: { + type: Schema.Types.String, + required: true, + }, }, { timestamps: true, diff --git a/server/src/data/database/model/PlaybooksRepository.ts b/server/src/data/database/model/PlaybooksRepository.ts index 01f27fb9..ad30e19f 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 { Repositories } from 'ssm-shared-lib'; +import { Repositories, SsmGit } from 'ssm-shared-lib'; export const DOCUMENT_NAME = 'PlaybooksRepository'; export const COLLECTION_NAME = 'playbooksrepository'; @@ -21,6 +21,7 @@ export default interface PlaybooksRepository { directoryExclusionList?: string[]; onError?: boolean; onErrorMessage?: string; + gitService?: SsmGit.Services; createdAt?: Date; updatedAt?: Date; } @@ -99,6 +100,10 @@ const schema = new Schema( type: Schema.Types.String, required: false, }, + gitService: { + type: Schema.Types.String, + required: false, + }, }, { timestamps: true, diff --git a/server/src/helpers/git/CREDIT.md b/server/src/helpers/git/CREDIT.md index bad05604..3d359b84 100644 --- a/server/src/helpers/git/CREDIT.md +++ b/server/src/helpers/git/CREDIT.md @@ -19,3 +19,5 @@ 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. + +https://github.com/tiddly-gittly/git-sync-js/tree/master diff --git a/server/src/helpers/git/clone.ts b/server/src/helpers/git/clone.ts index 9ddaba37..dfdbe8d7 100644 --- a/server/src/helpers/git/clone.ts +++ b/server/src/helpers/git/clone.ts @@ -19,7 +19,7 @@ export async function clone(options: { userInfo?: IGitUserInfos; }): Promise { const { dir, remoteUrl, userInfo, logger, defaultGitInfo = defaultDefaultGitInfo } = options; - const { gitUserName, branch } = userInfo ?? defaultGitInfo; + const { gitUserName, branch, gitService } = userInfo ?? defaultGitInfo; const { accessToken } = userInfo ?? {}; if (accessToken === '' || accessToken === undefined) { @@ -61,7 +61,7 @@ export async function clone(options: { const remoteName = await getRemoteName(dir, branch); logDebug(`Successfully Running git init for clone in dir ${dir}`, GitStep.PrepareClone); logProgress(GitStep.StartConfiguringGithubRemoteRepository); - await credentialOn(dir, remoteUrl, gitUserName, accessToken, remoteName); + await credentialOn(dir, remoteUrl, gitUserName, accessToken, remoteName, gitService); try { logProgress(GitStep.StartFetchingFromGithubRemote); const { stderr: pullStdError, exitCode } = await GitProcess.exec( diff --git a/server/src/helpers/git/commitAndSync.ts b/server/src/helpers/git/commitAndSync.ts index 2a646671..afa635d5 100644 --- a/server/src/helpers/git/commitAndSync.ts +++ b/server/src/helpers/git/commitAndSync.ts @@ -51,7 +51,7 @@ export async function commitAndSync(options: ICommitAndSyncOptions): Promise + +export const getGitLabUrlWithCredential = ( + rawUrl: string, + username: string, + accessToken: string, +): string => + trim( + rawUrl + .replaceAll('\n', '') + .replace('https://gitlab.com/', `https://${username}:${accessToken}@gitlab.com/`), + ); +export const getBitbucketUrlWithCredential = ( + rawUrl: string, + username: string, + accessToken: string, +): string => + trim( + rawUrl + .replaceAll('\n', '') + .replace('https://bitbucket.org/', `https://${username}:${accessToken}@bitbucket.org/`), + ); + +export const getAzureReposUrlWithCredential = ( + rawUrl: string, + username: string, + accessToken: string, +): string => + trim( + rawUrl + .replaceAll('\n', '') + .replace('https://dev.azure.com/', `https://${username}:${accessToken}@dev.azure.com/`), + ); + +const getUrlWithOutCredential = (urlWithCredential: string): string => trim(urlWithCredential.replace(/.+@/, 'https://')); /** @@ -35,14 +65,26 @@ export async function credentialOn( userName: string, accessToken: string, remoteName: string, - serviceType = ServiceType.Github, + serviceType: SsmGit.Services, ): Promise { let gitUrlWithCredential; switch (serviceType) { - case ServiceType.Github: { + case SsmGit.Services.Github: { gitUrlWithCredential = getGitHubUrlWithCredential(remoteUrl, userName, accessToken); break; } + case SsmGit.Services.GitLab: { + gitUrlWithCredential = getGitLabUrlWithCredential(remoteUrl, userName, accessToken); + break; + } + case SsmGit.Services.Bitbucket: { + gitUrlWithCredential = getBitbucketUrlWithCredential(remoteUrl, userName, accessToken); + break; + } + case SsmGit.Services.AzureRepos: { + gitUrlWithCredential = getAzureReposUrlWithCredential(remoteUrl, userName, accessToken); + break; + } } await GitProcess.exec(['remote', 'add', remoteName, gitUrlWithCredential], directory); await GitProcess.exec(['remote', 'set-url', remoteName, gitUrlWithCredential], directory); @@ -58,13 +100,25 @@ export async function credentialOff( directory: string, remoteName: string, remoteUrl?: string, - serviceType = ServiceType.Github, + serviceType = SsmGit.Services.Github, ): Promise { const gitRepoUrl = remoteUrl ?? (await getRemoteUrl(directory, remoteName)); let gitUrlWithOutCredential; switch (serviceType) { - case ServiceType.Github: { - gitUrlWithOutCredential = getGitHubUrlWithOutCredential(gitRepoUrl); + case SsmGit.Services.Github: { + gitUrlWithOutCredential = getUrlWithOutCredential(gitRepoUrl); + break; + } + case SsmGit.Services.GitLab: { + gitUrlWithOutCredential = getUrlWithOutCredential(gitRepoUrl); + break; + } + case SsmGit.Services.Bitbucket: { + gitUrlWithOutCredential = getUrlWithOutCredential(gitRepoUrl); + break; + } + case SsmGit.Services.AzureRepos: { + gitUrlWithOutCredential = getUrlWithOutCredential(gitRepoUrl); break; } } diff --git a/server/src/helpers/git/defaultGitInfo.ts b/server/src/helpers/git/defaultGitInfo.ts index 40ddb6b5..f7a4fe51 100644 --- a/server/src/helpers/git/defaultGitInfo.ts +++ b/server/src/helpers/git/defaultGitInfo.ts @@ -1,6 +1,9 @@ +import { SsmGit } from 'ssm-shared-lib'; + export const defaultGitInfo = { email: 'gitsync@gmail.com', gitUserName: 'gitsync', branch: 'main', remote: 'origin', + gitService: SsmGit.Services.Github, }; diff --git a/server/src/helpers/git/forcePull.ts b/server/src/helpers/git/forcePull.ts index 6575235d..e1031153 100644 --- a/server/src/helpers/git/forcePull.ts +++ b/server/src/helpers/git/forcePull.ts @@ -29,7 +29,7 @@ export interface IForcePullOptions { */ export async function forcePull(options: IForcePullOptions) { const { dir, logger, defaultGitInfo = defaultDefaultGitInfo, userInfo, remoteUrl } = options; - const { gitUserName, branch } = userInfo ?? defaultGitInfo; + const { gitUserName, branch, gitService } = userInfo ?? defaultGitInfo; const { accessToken } = userInfo ?? {}; const defaultBranchName = (await getDefaultBranchName(dir)) ?? branch; const remoteName = await getRemoteName(dir, branch); @@ -70,7 +70,7 @@ export async function forcePull(options: IForcePullOptions) { userInfo, }); logProgress(GitStep.StartConfiguringGithubRemoteRepository); - await credentialOn(dir, remoteUrl, gitUserName, accessToken, remoteName); + await credentialOn(dir, remoteUrl, gitUserName, accessToken, remoteName, gitService); try { logProgress(GitStep.StartFetchingFromGithubRemote); await fetchRemote(dir, defaultGitInfo.remote, defaultGitInfo.branch); diff --git a/server/src/helpers/git/interface.ts b/server/src/helpers/git/interface.ts index e28fb41d..55c6beb0 100644 --- a/server/src/helpers/git/interface.ts +++ b/server/src/helpers/git/interface.ts @@ -1,9 +1,12 @@ +import { SsmGit } from 'ssm-shared-lib'; + export interface IGitUserInfosWithoutToken { branch: string; /** Git commit message email */ email: string | null | undefined; /** Github Login: username , this is also used to filter user's repo when searching repo */ gitUserName: string; + gitService: SsmGit.Services; } export interface IGitUserInfos extends IGitUserInfosWithoutToken { diff --git a/server/src/modules/docker/CREDIT.md b/server/src/modules/docker/CREDIT.md new file mode 100644 index 00000000..6499657f --- /dev/null +++ b/server/src/modules/docker/CREDIT.md @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) [2019] [Manfred Martin] + +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. + +https://github.com/getwud/wud diff --git a/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts index 733ae8a2..1b3f1f41 100644 --- a/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts +++ b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts @@ -1,7 +1,7 @@ import pino from 'pino'; import shell from 'shelljs'; import { RepositoryType } from 'ssm-shared-lib/distribution/enums/repositories'; -import { SsmAlert } from 'ssm-shared-lib'; +import { SsmAlert, SsmGit } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import { SSM_DATA_PATH } from '../../config'; import EventManager from '../../core/events/EventManager'; @@ -43,6 +43,7 @@ class ContainerCustomStacksRepositoryComponent extends EventManager { gitUserName: string, accessToken: string, remoteUrl: string, + gitService: SsmGit.Services, ) { super(); const dir = `${DIRECTORY_ROOT}/${uuid}`; @@ -62,6 +63,7 @@ class ContainerCustomStacksRepositoryComponent extends EventManager { gitUserName: gitUserName, branch: branch, accessToken: accessToken, + gitService: gitService, }; this.options = { dir: this.directory, diff --git a/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts b/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts index ef939c1b..9021faf9 100644 --- a/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts +++ b/server/src/modules/repository/ContainerCustomStacksRepositoryEngine.ts @@ -28,7 +28,7 @@ export function getState(): stateType { async function registerGitRepository( containerCustomStackRepository: ContainerCustomStackRepository, ) { - const { uuid, name, branch, email, userName, accessToken, remoteUrl } = + const { uuid, name, branch, email, userName, accessToken, remoteUrl, gitService } = containerCustomStackRepository; if (!accessToken) { throw new Error('accessToken is required'); @@ -45,6 +45,7 @@ async function registerGitRepository( userName, decryptedAccessToken, remoteUrl, + gitService, ); } diff --git a/server/src/modules/repository/PlaybooksRepositoryEngine.ts b/server/src/modules/repository/PlaybooksRepositoryEngine.ts index 75fa9383..0f55cef1 100644 --- a/server/src/modules/repository/PlaybooksRepositoryEngine.ts +++ b/server/src/modules/repository/PlaybooksRepositoryEngine.ts @@ -30,7 +30,8 @@ export function getState(): stateType { } async function registerGitRepository(playbookRepository: PlaybooksRepository) { - const { uuid, name, branch, email, userName, accessToken, remoteUrl } = playbookRepository; + const { uuid, name, branch, email, userName, accessToken, remoteUrl, gitService } = + playbookRepository; if (!accessToken) { throw new Error('accessToken is required'); } @@ -48,6 +49,7 @@ async function registerGitRepository(playbookRepository: PlaybooksRepository) { userName, decryptedAccessToken, remoteUrl, + gitService, ); } diff --git a/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts index 3b50fb1f..0de6de47 100644 --- a/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts +++ b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts @@ -1,4 +1,4 @@ -import { SsmAlert } from 'ssm-shared-lib'; +import { SsmAlert, SsmGit } from 'ssm-shared-lib'; import Events from '../../../core/events/events'; import logger from '../../../logger'; import GitPlaybooksRepositoryUseCases from '../../../services/GitPlaybooksRepositoryUseCases'; @@ -32,6 +32,7 @@ class GitPlaybooksRepositoryComponent gitUserName: string, accessToken: string, remoteUrl: string, + gitService: SsmGit.Services, ) { super(uuid, name, DIRECTORY_ROOT); this.uuid = uuid; @@ -41,6 +42,7 @@ class GitPlaybooksRepositoryComponent gitUserName: gitUserName, branch: branch, accessToken: accessToken, + gitService: gitService, }; this.options = { dir: this.directory, @@ -67,7 +69,7 @@ class GitPlaybooksRepositoryComponent ...this.options, logger: { debug: (message: string, context: ILoggerContext): unknown => - this.childLogger.debug(message, { callerFunction: 'clone', ...context }), + this.childLogger.info(message, { callerFunction: 'clone', ...context }), warn: (message: string, context: ILoggerContext): unknown => this.childLogger.warn(message, { callerFunction: 'clone', ...context }), info: (message: GitStep, context: ILoggerContext): void => { diff --git a/server/src/services/GitCustomStacksRepositoryUseCases.ts b/server/src/services/GitCustomStacksRepositoryUseCases.ts index 28d2b866..f04b7a8e 100644 --- a/server/src/services/GitCustomStacksRepositoryUseCases.ts +++ b/server/src/services/GitCustomStacksRepositoryUseCases.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; +import { SsmGit } from 'ssm-shared-lib'; import ContainerCustomStackRepository from '../data/database/model/ContainerCustomStackRepository'; import ContainerCustomStackRepositoryRepo from '../data/database/repository/ContainerCustomStackRepositoryRepo'; import { InternalError } from '../middlewares/api/ApiError'; @@ -13,6 +14,7 @@ async function addGitRepository( email: string, userName: string, remoteUrl: string, + gitService: SsmGit.Services, matchesList?: string[], ) { const uuid = uuidv4(); @@ -26,6 +28,7 @@ async function addGitRepository( remoteUrl, enabled: true, matchesList, + gitService, }); await ContainerCustomStackRepositoryRepo.create({ uuid, @@ -37,6 +40,7 @@ async function addGitRepository( userName, enabled: true, matchesList, + gitService, }); void gitRepository.clone(true); } @@ -49,6 +53,7 @@ async function updateGitRepository( email: string, userName: string, remoteUrl: string, + gitService: SsmGit.Services, matchesList?: string[], ) { await ContainerCustomStacksRepositoryEngine.deregisterRepository(uuid); @@ -62,6 +67,7 @@ async function updateGitRepository( remoteUrl, enabled: true, matchesList, + gitService, }); await ContainerCustomStackRepositoryRepo.update({ uuid, @@ -73,6 +79,7 @@ async function updateGitRepository( userName, enabled: true, matchesList, + gitService, }); } diff --git a/server/src/services/GitPlaybooksRepositoryUseCases.ts b/server/src/services/GitPlaybooksRepositoryUseCases.ts index cf2f71b4..0d69e3ee 100644 --- a/server/src/services/GitPlaybooksRepositoryUseCases.ts +++ b/server/src/services/GitPlaybooksRepositoryUseCases.ts @@ -1,4 +1,4 @@ -import { Repositories } from 'ssm-shared-lib'; +import { Repositories, SsmGit } from 'ssm-shared-lib'; import { v4 as uuidv4 } from 'uuid'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; import PlaybooksRepositoryEngine from '../modules/repository/PlaybooksRepositoryEngine'; @@ -10,6 +10,7 @@ async function addGitRepository( email: string, userName: string, remoteUrl: string, + gitService: SsmGit.Services, directoryExclusionList?: string[], ) { const uuid = uuidv4(); @@ -24,6 +25,7 @@ async function addGitRepository( remoteUrl, enabled: true, directoryExclusionList, + gitService, }); await PlaybooksRepositoryRepo.create({ uuid, @@ -37,6 +39,7 @@ async function addGitRepository( directory: gitRepository.getDirectory(), enabled: true, directoryExclusionList, + gitService, }); void gitRepository.clone(true); } @@ -49,6 +52,7 @@ async function updateGitRepository( email: string, userName: string, remoteUrl: string, + gitService: SsmGit.Services, directoryExclusionList?: string[], ) { await PlaybooksRepositoryEngine.deregisterRepository(uuid); @@ -63,6 +67,7 @@ async function updateGitRepository( remoteUrl, enabled: true, directoryExclusionList, + gitService, }); await PlaybooksRepositoryRepo.update({ uuid, @@ -76,6 +81,7 @@ async function updateGitRepository( directory: gitRepository.getDirectory(), enabled: true, directoryExclusionList, + gitService, }); } diff --git a/shared-lib/src/enums/git.ts b/shared-lib/src/enums/git.ts new file mode 100644 index 00000000..3bcdbbcd --- /dev/null +++ b/shared-lib/src/enums/git.ts @@ -0,0 +1,6 @@ +export enum Services { + Github = 'github', + GitLab = 'gitlab', + Bitbucket = 'bitbucket', + AzureRepos = 'azure-repos' +} diff --git a/shared-lib/src/enums/settings.ts b/shared-lib/src/enums/settings.ts index fb93617b..ac3abb7a 100644 --- a/shared-lib/src/enums/settings.ts +++ b/shared-lib/src/enums/settings.ts @@ -12,7 +12,7 @@ export enum GeneralSettingsKeys { } export enum DefaultValue { - SCHEME_VERSION = '15', + SCHEME_VERSION = '16', SERVER_LOG_RETENTION_IN_DAYS = '30', CONSIDER_DEVICE_OFFLINE_AFTER_IN_MINUTES = '3', CONSIDER_PERFORMANCE_GOOD_MEM_IF_GREATER = '10', diff --git a/shared-lib/src/index.ts b/shared-lib/src/index.ts index 6a9ff135..29d49a33 100644 --- a/shared-lib/src/index.ts +++ b/shared-lib/src/index.ts @@ -12,4 +12,5 @@ export * as Automations from './form/automation'; export * as SsmEvents from './types/events'; export * as SsmAgent from './enums/agent'; export * as SsmAlert from './enums/alert'; +export * as SsmGit from './enums/git'; export * as SsmDeviceDiagnostic from './enums/diagnostic' diff --git a/shared-lib/src/types/api.ts b/shared-lib/src/types/api.ts index 860d31c9..f619447f 100644 --- a/shared-lib/src/types/api.ts +++ b/shared-lib/src/types/api.ts @@ -1,5 +1,6 @@ import { ExtraVarsType, SSHConnection, SSHType } from '../enums/ansible'; import { VolumeBackupMode } from '../enums/container'; +import { Services } from '../enums/git'; import { RepositoryType } from '../enums/repositories'; import { AutomationChain } from '../form/automation'; import { ExtendedTreeNode } from './tree'; @@ -666,6 +667,8 @@ export type GitPlaybooksRepository = PlaybooksRepository & { userName: string; remoteUrl: string; default: boolean; + gitService: Services; + accessToken?: string; } export type LocalPlaybooksRepository = PlaybooksRepository & { @@ -685,6 +688,8 @@ export type GitContainerStacksRepository = { matchesList?: string[]; onError?: boolean; onErrorMessage?: string; + gitService: Services; + accessToken?: string; } export type ExtraVars = ExtraVar[]; diff --git a/site/.vitepress/config.ts b/site/.vitepress/config.ts index 193011b8..9975755a 100644 --- a/site/.vitepress/config.ts +++ b/site/.vitepress/config.ts @@ -149,6 +149,7 @@ export default defineConfig({ { text: 'Stacks - Container', items: [ { text: 'Overview', link: '/docs/compose/editor.md' }, + { text: 'Remote Container Stacks Repositories', link: '/docs/compose/remote-stacks.md' } ] }, { diff --git a/site/docs/compose/remote-stacks.md b/site/docs/compose/remote-stacks.md new file mode 100644 index 00000000..4c99b4f9 --- /dev/null +++ b/site/docs/compose/remote-stacks.md @@ -0,0 +1,33 @@ +# Remote Stacks Repositories + +Remote stacks repositories are Git repositories that will be cloned to your filesystem as separate folders. + +## Adding a new remote repository + +To add a Git playbooks repository, you must provide the following information: +- The `Name` of the repo that will be displayed in the Playbooks page +- The `Git Service` of the repo (e.g: `Github`, `Gitlab`, `Azure`, `Gitbucket`) +- The `Git email` associated with the access token +- The `Git username` associated with the access token +- The `Branch` to checkout and push changes to (e.g., `master` or `main`) +- The `Access Token` associated with the user +- `Match files`: SSM will only import files matching the given patterns + + +![add-file](/compose/add-remote-options-2.png) + +## Synchronization + +- SSM **will not** listen to changes (addition/deletion) made outside its interface unless a manual synchronization is triggered. +- Any changes made **inside** SSM will be automatically synchronized. + If you believe SSM is desynchronized from the actual file structure of the repository, click on `...` of the `Actions` button of the Repository modal, and click on `Sync to database` + +![add-file](/playbooks/manual-sync.gif) + +## Delete a remote repository + +:::warning ⚠️ Destructive action +This action is permanent. Deleting a remote repository will effectively delete the underlying files and directories permanently. Proceed with caution! +::: + +![add-file](/playbooks/delete-repo.png) diff --git a/site/docs/playbooks/remote-playbooks.md b/site/docs/playbooks/remote-playbooks.md index 3f164cfe..c14a0a2f 100644 --- a/site/docs/playbooks/remote-playbooks.md +++ b/site/docs/playbooks/remote-playbooks.md @@ -7,10 +7,14 @@ Remote playbooks repositories are Git repositories that will be cloned to your f ![add-file](/playbooks/add-remote.gif) To add a Git playbooks repository, you must provide the following information: -- The `name` of the repo that will be displayed in the Playbooks page +- The `Name` of the repo that will be displayed in the Playbooks page +- The `Git Service` of the repo (e.g: `Github`, `Gitlab`, `Azure`, `Gitbucket`) - The `Git email` associated with the access token - The `Git username` associated with the access token -- The `branch` to checkout and push changes to (e.g., `master` or `main`) +- The `Branch` to checkout and push changes to (e.g., `master` or `main`) +- The `Access Token` associated with the user + +- `Exclude Directories from Execution List`: All files locates withing those paths will be excluded for the list of playbooks you can execute in SSM. It can be usefull to exclude folders containing roles, vars, etc... ![add-file](/playbooks/add-remote-options.png) diff --git a/site/public/compose/add-remote-options-2.png b/site/public/compose/add-remote-options-2.png new file mode 100644 index 00000000..12ed73ac Binary files /dev/null and b/site/public/compose/add-remote-options-2.png differ diff --git a/site/public/playbooks/add-remote-options.png b/site/public/playbooks/add-remote-options.png index 8ee2c326..6db0f29d 100644 Binary files a/site/public/playbooks/add-remote-options.png and b/site/public/playbooks/add-remote-options.png differ