mirror of
https://github.com/caprover/caprover
synced 2026-06-19 07:37:08 +00:00
Release 1.13 (#2166)
* Fixed https://github.com/caprover/caprover/issues/2112
* Updated changelog
* Force rebuild to capture the frontend change
* Updated changelog
* added fallback ip for installation if public ip not found
* Added project definition to the config
* Added project router
* Updated apps router
* Fixed update path
* Added parent ID for projects
* Typo fix
* Not allow parent to be deleted if there are sub projects
* Fixed typo in string template
* Allow register project to accept description
* Added functionality to remove multiple projects
* Fixed the project deletion
* Fixed the project deletion
* Close https://github.com/caprover/caprover/issues/2153
* Added changelog
* updated changelog
* Updated changelog
* Better error
* Force build
* add MariaDB to README.md
* Improved MacOs startup
* Added theme on backend (#2161)
* Added theme on backend
* Added themes
* Added another theme
* Updated changelog
* Reformat
* Improved dev code
* Improved dev code
* Added another theme
* Updated packages (#2165)
* Updated packages
* Updated packages
* Fixed formatting
* Fixed tests
* Returning project upon creation
* Preparing release
* updated frontend
* Added some test to fix node 20
* Revert "Added some test to fix node 20"
This reverts commit 18a59794b8.
* Updated changelog
---------
Co-authored-by: Robert Silén <robert.silen@mariadb.org>
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
built
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
@@ -1,23 +0,0 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-useless-escape": "off",
|
||||
|
||||
}
|
||||
};
|
||||
Vendored
+9
-1
@@ -9,12 +9,20 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.addMissingImports": "explicit"
|
||||
},
|
||||
"typescript.referencesCodeLens.enabled": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"cSpell.words": ["csrf", "definitelytyped", "dockerode", "promisified"],
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[plaintext]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.defaultFormatter": null
|
||||
},
|
||||
"files.associations": {
|
||||
"*.theme": "plaintext"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
- N/A
|
||||
|
||||
## [1.13.0] - 2024-10-19
|
||||
|
||||
- New: Added project structure [issue-158](https://github.com/caprover/caprover-frontend/pull/158)
|
||||
- New: Added translations in multiple languages [issue-159](https://github.com/caprover/caprover-frontend/pull/159)
|
||||
- New: Added themes! Now you can pick your favorite theme or build a new one! [issue-160](https://github.com/caprover/caprover-frontend/pull/160)
|
||||
- New: Added automatic IP fallback on installation [ca196e5](https://github.com/caprover/caprover/commit/ca196e51be2df80836ff027a99bb92dde83c4f7f)
|
||||
- New: Disallow passphrase protected SSH keys [issue-2153](https://github.com/caprover/caprover/issues/2153)
|
||||
- Fixed: Deploy time now uses the proper locale [issue-157](https://github.com/caprover/caprover-frontend/issues/157)
|
||||
- Fixed: The app log box is resizable again [issue-2112](https://github.com/caprover/caprover/issues/2112)
|
||||
- Fixed: Showing the missing timezones due to daylight saving time [issue-2110](https://github.com/caprover/caprover/issues/2110)
|
||||
- Fixed: Log Search filter crash on invalid Regex [issue-2128](https://github.com/caprover/caprover/issues/2128)
|
||||
|
||||
## [1.12.0] - 2024-08-17
|
||||
|
||||
**IMPORTANT**: this version bumps the minimum Docker API to 1.43. Please run `docker version | grep API` before upgrading your CapRover installation.
|
||||
|
||||
@@ -14,8 +14,6 @@ Easiest app/database deployment platform and webserver package for your NodeJS,
|
||||
|
||||
No Docker, nginx knowledge required!
|
||||
|
||||
[](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fcaprover%2Fcaprover&via=cap_rover&text=I%20found%20the%20easiest%20webserver%20package%20for%20NodeJS%2C%20PHP%2C%20MySQL%2C%20WordPress%20and%20everything%21&hashtags=CapRover%2Cnodejs%2Cdocker%2Cnginx%2Cwebdev)
|
||||
|
||||
<a href="https://youtu.be/VPHEXPfsvyQ" target="_blank" title="YouTube">
|
||||
<img src="https://raw.githubusercontent.com/caprover/caprover-website/master/graphics/screenshots-video-small.png" alt="YouTube"/>
|
||||
</a>
|
||||
@@ -23,7 +21,7 @@ No Docker, nginx knowledge required!
|
||||
|
||||
## What's this?
|
||||
|
||||
CapRover is an extremely easy to use app/database deployment & web server manager for your **NodeJS, Python, PHP, ASP.NET, Ruby, MySQL, MongoDB, Postgres, WordPress (and etc...)** applications!
|
||||
CapRover is an extremely easy to use app/database deployment & web server manager for your **NodeJS, Python, PHP, ASP.NET, Ruby, MariaDB, MySQL, MongoDB, Postgres, WordPress (and etc...)** applications!
|
||||
|
||||
It's blazingly fast and very robust as it uses Docker, nginx, LetsEncrypt and NetData under the hood behind its simple-to-use interface.
|
||||
|
||||
@@ -44,7 +42,7 @@ It's blazingly fast and very robust as it uses Docker, nginx, LetsEncrypt and Ne
|
||||
- A [web] developer who does not like spending hours and days setting up a server, build tools, sending code to server, build it, get an SSL certificate, install it, update nginx over and over again.
|
||||
- A developer who uses expensive services like Heroku, Microsoft Azure and etc. And is interested in reducing their cost by 50x (Heroku charges 250USD/month for their 2gb instance, the same server is 5$ on Hetzner!!)
|
||||
- Someone who prefers to write more of `showResults(getUserList())` and not much of `$ apt-get install libstdc++6 > /dev/null`
|
||||
- A developer who likes installing MySQL, MongoDB and etc on their server by selecting from a dropdown and clicking on install!
|
||||
- A developer who likes installing MariaDB, MySQL, MongoDB and etc on their server by selecting from a dropdown and clicking on install!
|
||||
- How much server/docker/linux knowledge is required to set up a CapRover server? Answer: Knowledge of Copy & Paste!! Head over to "Getting Started" for information on what to copy & paste ;-)
|
||||
|
||||
## Learn More!
|
||||
|
||||
@@ -48,9 +48,12 @@ echo "Building finished"
|
||||
cd $ORIG_DIR
|
||||
mv $FRONTEND_DIR/caprover-frontend/build ./dist-frontend
|
||||
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
sudo apt-get update && sudo apt-get install qemu-user-static
|
||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
docker run --rm --privileged tonistiigi/binfmt --install all
|
||||
# export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx ls
|
||||
docker buildx rm mybuilder || echo "mybuilder not found"
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ echo $IMAGE_NAME:$CAPROVER_VERSION
|
||||
echo "**************************************"
|
||||
echo "**************************************"
|
||||
|
||||
FRONTEND_COMMIT_HASH=89516709d5462c38554cae5b62845432adf3f88a
|
||||
FRONTEND_COMMIT_HASH=9f6c377137088c10b5582c4c9cd8285a9bd450d9
|
||||
|
||||
## Building frontend app
|
||||
ORIG_DIR=$(pwd)
|
||||
@@ -62,9 +62,12 @@ echo "Building finished"
|
||||
cd $ORIG_DIR
|
||||
mv $FRONTEND_DIR/caprover-frontend/build ./dist-frontend
|
||||
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
sudo apt-get update && sudo apt-get install qemu-user-static
|
||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
docker run --rm --privileged tonistiigi/binfmt --install all
|
||||
# export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx ls
|
||||
docker buildx rm mybuilder || echo "mybuilder not found"
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
if ! [ $(id -u) <> 0 ]; then
|
||||
if ! [ $(id -u) ] <>0; then
|
||||
echo "Must not be run as sudo or root on macos (macos security) please run the step 1 as root and this step as standard user"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pwd >currentdirectory
|
||||
docker service rm $(docker service ls -q)
|
||||
sleep 1
|
||||
docker secret rm captain-salt
|
||||
|
||||
@@ -4,6 +4,7 @@ RUN apk add --update --no-cache make gcc g++ git curl openssl openssh
|
||||
WORKDIR /usr/src/app
|
||||
COPY . ./
|
||||
|
||||
|
||||
# Build backend code
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import pluginJs from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
"^.+\\.ts?$": "ts-jest",
|
||||
|
||||
Generated
+3775
-2616
File diff suppressed because it is too large
Load Diff
+37
-33
@@ -4,80 +4,84 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"disable-otp": "node ./built/scripts/disable-otp.js",
|
||||
"dev": "npm run build && sudo ./dev-scripts/dev-reset-service.sh",
|
||||
"dev": "sudo 'echo' && npm run build && sudo ./dev-scripts/dev-reset-service.sh",
|
||||
"clean": "npm run build && sudo ./dev-scripts/dev-clean-run-as-dev.sh",
|
||||
"lint": "eslint -c .eslintrc.js --ext .ts ./src",
|
||||
"lint-fix": "eslint --fix -c .eslintrc.js --ext .ts ./src",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"lint-fix": "eslint --fix 'src/**/*.ts'",
|
||||
"formatter": "prettier --check './src/**/*.ts'",
|
||||
"formatter-write": "prettier --write './src/**/*.ts'",
|
||||
"build": "echo 'RECOMPILING' && npx madge --circular --extensions ts ./ && rm -rf ./built && npx tsc && echo 'Build successful'",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"configstore": "^5.0.1",
|
||||
"cookie-parser": "~1.4.6",
|
||||
"cookie-parser": "~1.4.7",
|
||||
"cron": "^3.1.7",
|
||||
"debug": "~4.3.6",
|
||||
"dockerode": "^3.3.5",
|
||||
"debug": "~4.3.7",
|
||||
"dockerode": "^4.0.2",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"express": "^4.21.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.4",
|
||||
"on-finished": "^2.4.1",
|
||||
"prettier": "2.8.8",
|
||||
"public-ip": "^4.0.4",
|
||||
"prettier": "3.3.3",
|
||||
"public-ip": "^7.0.1",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"request": "^2.88.2",
|
||||
"require-from-string": "^2.0.2",
|
||||
"serve-favicon": "~2.5.0",
|
||||
"shell-quote": "^1.8.1",
|
||||
"simple-git": "^2.48.0",
|
||||
"ssh2": "^1.15.0",
|
||||
"tar": "^6.2.1",
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^8.3.2",
|
||||
"simple-git": "^3.27.0",
|
||||
"ssh2": "^1.16.0",
|
||||
"tar": "^7.4.3",
|
||||
"typescript": "^5.6.3",
|
||||
"uuid": "^10.0.0",
|
||||
"validator": "^13.12.0",
|
||||
"yaml": "^1.10.2"
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/configstore": "^5.0.1",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/dockerode": "^3.3.31",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/http-proxy": "^1.17.15",
|
||||
"@types/is-valid-path": "^0.1.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/js-base64": "^3.3.1",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/on-finished": "^2.3.4",
|
||||
"@types/request": "^2.48.12",
|
||||
"@types/require-from-string": "^1.2.3",
|
||||
"@types/serve-favicon": "^2.5.7",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ssh2": "^0.5.52",
|
||||
"@types/ssh2": "^1.15.1",
|
||||
"@types/tar": "^6.1.13",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/validator": "^13.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^27.5.1",
|
||||
"madge": "^5.0.2",
|
||||
"ts-jest": "^27.1.5"
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/validator": "^13.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.10.0",
|
||||
"@typescript-eslint/parser": "^8.10.0",
|
||||
"eslint": "^9.12.0",
|
||||
"globals": "^15.11.0",
|
||||
"jest": "^29.7.0",
|
||||
"madge": "^8.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript-eslint": "^8.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
class BaseApi {
|
||||
public data: any
|
||||
|
||||
constructor(public status: number, public description: string) {
|
||||
constructor(
|
||||
public status: number,
|
||||
public description: string
|
||||
) {
|
||||
this.data = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import InjectionExtractor from './injection/InjectionExtractor'
|
||||
import * as Injector from './injection/Injector'
|
||||
import DownloadRouter from './routes/download/DownloadRouter'
|
||||
import LoginRouter from './routes/login/LoginRouter'
|
||||
import ThemePublicRouter from './routes/public/ThemePublicRouter'
|
||||
import UserRouter from './routes/user/UserRouter'
|
||||
import CaptainManager from './user/system/CaptainManager'
|
||||
import CaptainConstants from './utils/CaptainConstants'
|
||||
@@ -215,6 +216,7 @@ app.use(
|
||||
API_PREFIX + CaptainConstants.apiVersion + '/downloads/',
|
||||
DownloadRouter
|
||||
)
|
||||
app.use(API_PREFIX + CaptainConstants.apiVersion + '/theme/', ThemePublicRouter)
|
||||
|
||||
// secured end points
|
||||
app.use(API_PREFIX + CaptainConstants.apiVersion + '/user/', UserRouter)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import {
|
||||
AppDeployTokenConfig,
|
||||
IAllAppDefinitions,
|
||||
IAppDef,
|
||||
IAppDefSaved,
|
||||
IAppEnvVar,
|
||||
IAppPort,
|
||||
IAppTag,
|
||||
IAppVersion,
|
||||
IAppVolume,
|
||||
IHttpAuth,
|
||||
RepoInfo,
|
||||
} from '../models/AppDefinition'
|
||||
import { IBuiltImage } from '../models/IBuiltImage'
|
||||
import Authenticator from '../user/Authenticator'
|
||||
import ApacheMd5 from '../utils/ApacheMd5'
|
||||
@@ -31,7 +44,10 @@ function isPortValid(portNumber: number) {
|
||||
class AppsDataStore {
|
||||
private encryptor: CaptainEncryptor
|
||||
|
||||
constructor(private data: configstore, private namepace: string) {}
|
||||
constructor(
|
||||
private data: configstore,
|
||||
private namepace: string
|
||||
) {}
|
||||
|
||||
setEncryptor(encryptor: CaptainEncryptor) {
|
||||
this.encryptor = encryptor
|
||||
@@ -611,6 +627,7 @@ class AppsDataStore {
|
||||
|
||||
updateAppDefinitionInDb(
|
||||
appName: string,
|
||||
projectId: string | undefined,
|
||||
description: string,
|
||||
instanceCount: number,
|
||||
captainDefinitionRelativeFilePath: string,
|
||||
@@ -705,6 +722,7 @@ class AppsDataStore {
|
||||
appObj.preDeployFunction = preDeployFunction
|
||||
appObj.serviceUpdateOverride = serviceUpdateOverride
|
||||
appObj.description = description
|
||||
appObj.projectId = projectId
|
||||
appObj.tags = tags
|
||||
|
||||
appObj.appDeployTokenConfig = {
|
||||
@@ -860,7 +878,11 @@ class AppsDataStore {
|
||||
* @param hasPersistentData whether the app has persistent data, you can only run one instance of the app.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
registerAppDefinition(appName: string, hasPersistentData: boolean) {
|
||||
registerAppDefinition(
|
||||
appName: string,
|
||||
projectId: string | undefined,
|
||||
hasPersistentData: boolean
|
||||
) {
|
||||
const self = this
|
||||
|
||||
return new Promise<IAppDef>(function (resolve, reject) {
|
||||
@@ -886,6 +908,7 @@ class AppsDataStore {
|
||||
|
||||
const defaultAppDefinition: IAppDef = {
|
||||
hasPersistentData: !!hasPersistentData,
|
||||
projectId: projectId,
|
||||
description: '',
|
||||
instanceCount: 1,
|
||||
captainDefinitionRelativeFilePath:
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
AutomatedCleanupConfigsCleaner,
|
||||
IAutomatedCleanupConfigs,
|
||||
} from '../models/AutomatedCleanupConfigs'
|
||||
import CapRoverTheme from '../models/CapRoverTheme'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import CaptainEncryptor from '../utils/Encryptor'
|
||||
import Utils from '../utils/Utils'
|
||||
import AppsDataStore from './AppsDataStore'
|
||||
import ProDataStore from './ProDataStore'
|
||||
import ProjectsDataStore from './ProjectsDataStore'
|
||||
import RegistriesDataStore from './RegistriesDataStore'
|
||||
import { NetDataInfo } from '../models/NetDataInfo'
|
||||
|
||||
// keys:
|
||||
const NAMESPACE = 'namespace'
|
||||
@@ -27,6 +31,8 @@ const NGINX_CAPTAIN_CONFIG = 'nginxCaptainConfig'
|
||||
const CUSTOM_ONE_CLICK_APP_URLS = 'oneClickAppUrls'
|
||||
const FEATURE_FLAGS = 'featureFlags'
|
||||
const AUTOMATED_CLEANUP = 'automatedCleanup'
|
||||
const THEMES = 'themes'
|
||||
const CURRENT_THEME = 'currentTheme'
|
||||
|
||||
const DEFAULT_CAPTAIN_ROOT_DOMAIN = 'captain.localhost'
|
||||
|
||||
@@ -58,6 +64,7 @@ class DataStore {
|
||||
private appsDataStore: AppsDataStore
|
||||
private registriesDataStore: RegistriesDataStore
|
||||
proDataStore: ProDataStore
|
||||
private projectsDataStore: ProjectsDataStore
|
||||
|
||||
constructor(namespace: string) {
|
||||
const data = new Configstore(
|
||||
@@ -72,6 +79,10 @@ class DataStore {
|
||||
this.namespace = namespace
|
||||
this.data.set(NAMESPACE, namespace)
|
||||
this.appsDataStore = new AppsDataStore(this.data, namespace)
|
||||
this.projectsDataStore = new ProjectsDataStore(
|
||||
this.data,
|
||||
this.appsDataStore
|
||||
)
|
||||
this.proDataStore = new ProDataStore(this.data)
|
||||
this.registriesDataStore = new RegistriesDataStore(this.data, namespace)
|
||||
}
|
||||
@@ -98,6 +109,54 @@ class DataStore {
|
||||
})
|
||||
}
|
||||
|
||||
getThemes(): Promise<CapRoverTheme[]> {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.data.get(THEMES) || []
|
||||
})
|
||||
}
|
||||
|
||||
deleteTheme(themeName: string) {
|
||||
const self = this
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.getThemes()
|
||||
})
|
||||
.then(function (themesFetched) {
|
||||
self.data.set(
|
||||
THEMES,
|
||||
Utils.copyObject(themesFetched).filter(
|
||||
(it) => it.name !== themeName
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
saveThemes(themes: CapRoverTheme[]) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
self.data.set(
|
||||
THEMES,
|
||||
(themes || []).filter((it) => !it.builtIn)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setCurrentTheme(themeName: string | undefined) {
|
||||
const self = this
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return self.data.set(CURRENT_THEME, themeName || '')
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentThemeName(): Promise<string | undefined> {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
return self.data.get(CURRENT_THEME)
|
||||
})
|
||||
}
|
||||
|
||||
setHashedPassword(newHashedPassword: string) {
|
||||
const self = this
|
||||
return Promise.resolve().then(function () {
|
||||
@@ -117,7 +176,7 @@ class DataStore {
|
||||
return Promise.resolve().then(function () {
|
||||
return self.data.set(
|
||||
AUTOMATED_CLEANUP,
|
||||
AutomatedCleanupConfigsCleaner.cleanup(configs)
|
||||
AutomatedCleanupConfigsCleaner.sanitizeInput(configs)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -127,7 +186,7 @@ class DataStore {
|
||||
return Promise.resolve().then(function () {
|
||||
return (
|
||||
self.data.get(AUTOMATED_CLEANUP) ||
|
||||
AutomatedCleanupConfigsCleaner.cleanup({
|
||||
AutomatedCleanupConfigsCleaner.sanitizeInput({
|
||||
mostRecentLimit: 0,
|
||||
cronSchedule: '',
|
||||
timezone: '',
|
||||
@@ -195,6 +254,10 @@ class DataStore {
|
||||
return this.appsDataStore
|
||||
}
|
||||
|
||||
getProjectsDataStore() {
|
||||
return this.projectsDataStore
|
||||
}
|
||||
|
||||
getProDataStore() {
|
||||
return this.proDataStore
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import DataStore from './DataStore'
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import configstore = require('configstore')
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import { ProjectDefinition } from '../models/ProjectDefinition'
|
||||
import Utils from '../utils/Utils'
|
||||
import AppsDataStore from './AppsDataStore'
|
||||
|
||||
const PROJECTS_DEFINITIONS = 'projectsDefinitions'
|
||||
|
||||
function isNameAllowed(name: string) {
|
||||
const isNameFormattingOk =
|
||||
!!name &&
|
||||
name.length < 50 &&
|
||||
/^[a-z]/.test(name) &&
|
||||
/[a-z0-9]$/.test(name) &&
|
||||
/^[a-z0-9\-]+$/.test(name) &&
|
||||
name.indexOf('--') < 0
|
||||
return isNameFormattingOk && ['captain', 'root'].indexOf(name) < 0
|
||||
}
|
||||
|
||||
function isValidUUID(uuid: string | undefined): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
return !!uuid && uuidRegex.test(uuid)
|
||||
}
|
||||
|
||||
class ProjectsDataStore {
|
||||
constructor(
|
||||
private data: configstore,
|
||||
private appsDataStore: AppsDataStore
|
||||
) {}
|
||||
|
||||
saveProject(projectId: string, project: ProjectDefinition) {
|
||||
const self = this
|
||||
projectId = `${projectId || ''}`.trim()
|
||||
|
||||
return Promise.resolve()
|
||||
|
||||
.then(function () {
|
||||
return self.getAllProjects()
|
||||
})
|
||||
.then(function (allProjects) {
|
||||
project.name = `${project.name || ''}`.trim()
|
||||
project.id = `${project.id || ''}`.trim()
|
||||
project.description = `${project.description || ''}`.trim()
|
||||
project.parentProjectId = `${
|
||||
project.parentProjectId || ''
|
||||
}`.trim()
|
||||
|
||||
if (!isNameAllowed(project.name)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project name is not allowed'
|
||||
)
|
||||
}
|
||||
|
||||
if (project.id !== projectId) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project ID does not match'
|
||||
)
|
||||
}
|
||||
|
||||
if (!isValidUUID(project.id)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project ID is not a valid UUID'
|
||||
)
|
||||
}
|
||||
|
||||
if (project.parentProjectId) {
|
||||
if (project.parentProjectId === project.id) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Parent Project ID cannot be the same as the project ID'
|
||||
)
|
||||
}
|
||||
|
||||
if (!isValidUUID(project.parentProjectId)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Parent Project ID is not a valid UUID'
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
!allProjects.some(
|
||||
(p) => p.id === project.parentProjectId
|
||||
)
|
||||
) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Parent Project ID does not exist'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const projectToSave: ProjectDefinition = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
parentProjectId: project.parentProjectId,
|
||||
description: project.description,
|
||||
}
|
||||
|
||||
self.data.set(
|
||||
`${PROJECTS_DEFINITIONS}.${projectId}`,
|
||||
projectToSave
|
||||
)
|
||||
|
||||
return projectToSave
|
||||
})
|
||||
}
|
||||
|
||||
getAllProjects(): Promise<ProjectDefinition[]> {
|
||||
const self = this
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.data.get(PROJECTS_DEFINITIONS)
|
||||
})
|
||||
.then(function (projects) {
|
||||
projects = projects || {}
|
||||
return Object.keys(projects).map((key) => projects[key]) || []
|
||||
})
|
||||
}
|
||||
|
||||
getProject(projectId: string): Promise<ProjectDefinition> {
|
||||
const self = this
|
||||
projectId = `${projectId || ''}`.trim()
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.data.get(`${PROJECTS_DEFINITIONS}.${projectId}`) as
|
||||
| ProjectDefinition
|
||||
| undefined
|
||||
})
|
||||
.then(function (project) {
|
||||
if (!project) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project not found'
|
||||
)
|
||||
}
|
||||
return project
|
||||
})
|
||||
}
|
||||
|
||||
organizeFromTheLeafsToRoot(input: ProjectDefinition[]) {
|
||||
const projectMap = new Map<string, ProjectDefinition>()
|
||||
input.forEach((project) => projectMap.set(project.id, project))
|
||||
|
||||
// Function to recursively organize projects
|
||||
const organizeProjects = (projectId: string): ProjectDefinition[] => {
|
||||
const project = projectMap.get(projectId)
|
||||
if (!project) return []
|
||||
|
||||
const children = Array.from(projectMap.values())
|
||||
.filter((p) => p.parentProjectId === projectId)
|
||||
// just to make this deterministic for testing!
|
||||
.sort((a, b) => b.id.localeCompare(a.id))
|
||||
|
||||
const organizedChildren = children
|
||||
.map((child) => organizeProjects(child.id))
|
||||
.flat()
|
||||
|
||||
return [project, ...organizedChildren]
|
||||
}
|
||||
|
||||
const rootProjects = input
|
||||
.filter((project) => !project.parentProjectId)
|
||||
// just to make this deterministic for testing!
|
||||
.sort((a, b) => b.id.localeCompare(a.id))
|
||||
|
||||
return rootProjects
|
||||
.flatMap((root) => organizeProjects(root.id))
|
||||
.reverse()
|
||||
}
|
||||
|
||||
deleteProjects(projectIds: string[]) {
|
||||
const self = this
|
||||
|
||||
projectIds = projectIds || []
|
||||
projectIds = projectIds.map((it) => it.trim()).filter((it) => !!it)
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.getAllProjects()
|
||||
})
|
||||
.then(function (allProjects) {
|
||||
allProjects = self.organizeFromTheLeafsToRoot(allProjects)
|
||||
allProjects = allProjects.filter((project) =>
|
||||
projectIds.includes(project.id)
|
||||
)
|
||||
|
||||
const promises: Promise<any>[] = allProjects.map((p) =>
|
||||
self.deleteProject(p.id)
|
||||
)
|
||||
|
||||
return promises.reduce((accumulatedPromise, currentPromise) => {
|
||||
return accumulatedPromise.then(() => currentPromise)
|
||||
}, Promise.resolve())
|
||||
})
|
||||
}
|
||||
|
||||
deleteProject(projectId: string) {
|
||||
const self = this
|
||||
|
||||
projectId = `${projectId || ''}`.trim()
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
// dumb configstore needs some time to store the file!!!
|
||||
// otherwise (in case multiple deletes), the child deletion is not committed yet
|
||||
return Utils.getDelayedPromise(500)
|
||||
})
|
||||
.then(function () {
|
||||
return self.getProject(projectId)
|
||||
})
|
||||
.then(function (project) {
|
||||
// project exists
|
||||
|
||||
return self.appsDataStore.getAppDefinitions()
|
||||
})
|
||||
.then(function (appsAll) {
|
||||
const apps = Object.keys(appsAll).map((key) => appsAll[key])
|
||||
|
||||
if (apps.some((app) => app.projectId === projectId)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project is not empty (has apps)'
|
||||
)
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
return self.getAllProjects()
|
||||
})
|
||||
.then(function (allProjects) {
|
||||
if (allProjects.some((p) => p.parentProjectId === projectId)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project is not empty (has sub projects)'
|
||||
)
|
||||
}
|
||||
|
||||
return self.data.delete(`${PROJECTS_DEFINITIONS}.${projectId}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectsDataStore
|
||||
@@ -15,7 +15,10 @@ const DEFAULT_DOCKER_REGISTRY_ID = 'defaultDockerRegId'
|
||||
class RegistriesDataStore {
|
||||
private encryptor: CaptainEncryptor
|
||||
|
||||
constructor(private data: configstore, public namepace: string) {}
|
||||
constructor(
|
||||
private data: configstore,
|
||||
public namepace: string
|
||||
) {}
|
||||
|
||||
setEncryptor(encryptor: CaptainEncryptor) {
|
||||
this.encryptor = encryptor
|
||||
|
||||
+24
-13
@@ -1,20 +1,30 @@
|
||||
import Base64Provider = require('js-base64')
|
||||
import Docker = require('dockerode')
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import {
|
||||
IAppDef,
|
||||
IAppEnvVar,
|
||||
IAppPort,
|
||||
IAppVolume,
|
||||
} from '../models/AppDefinition'
|
||||
import { DockerAuthObj, DockerRegistryConfig } from '../models/DockerAuthObj'
|
||||
import { DockerSecret } from '../models/DockerSecret'
|
||||
import DockerService from '../models/DockerService'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import {
|
||||
IDockerApiPort,
|
||||
IDockerContainerResource,
|
||||
PreDeployFunction,
|
||||
VolumesTypes,
|
||||
} from '../models/OtherTypes'
|
||||
import { ServerDockerInfo } from '../models/ServerDockerInfo'
|
||||
import BuildLog from '../user/BuildLog'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import EnvVars from '../utils/EnvVars'
|
||||
import Logger from '../utils/Logger'
|
||||
import Utils from '../utils/Utils'
|
||||
import Dockerode = require('dockerode')
|
||||
// @ts-ignore
|
||||
// @ts-expect-error "TODO"
|
||||
import dockerodeUtils = require('dockerode/lib/util')
|
||||
|
||||
const Base64 = Base64Provider.Base64
|
||||
@@ -1425,11 +1435,11 @@ class DockerApi {
|
||||
|
||||
switch (updateOrder) {
|
||||
case IDockerUpdateOrders.AUTO:
|
||||
const existingVols =
|
||||
updatedData.TaskTemplate.ContainerSpec.Mounts ||
|
||||
[]
|
||||
updatedData.UpdateConfig.Order =
|
||||
existingVols.length > 0
|
||||
(
|
||||
updatedData.TaskTemplate.ContainerSpec
|
||||
.Mounts || []
|
||||
).length > 0
|
||||
? 'stop-first'
|
||||
: 'start-first'
|
||||
break
|
||||
@@ -1440,6 +1450,7 @@ class DockerApi {
|
||||
updatedData.UpdateConfig.Order = 'stop-first'
|
||||
break
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
const neverHappens: never = updateOrder
|
||||
throw new Error(
|
||||
`Unknown update order! ${updateOrder}${neverHappens}`
|
||||
@@ -1687,14 +1698,14 @@ const connectionParams: Docker.DockerOptions =
|
||||
socketPath: CaptainConstants.dockerSocketPath,
|
||||
}
|
||||
: dockerApiAddressSplited.length === 2
|
||||
? {
|
||||
host: dockerApiAddressSplited[0],
|
||||
port: Number(dockerApiAddressSplited[1]),
|
||||
}
|
||||
: {
|
||||
host: `${dockerApiAddressSplited[0]}:${dockerApiAddressSplited[1]}`,
|
||||
port: Number(dockerApiAddressSplited[2]),
|
||||
}
|
||||
? {
|
||||
host: dockerApiAddressSplited[0],
|
||||
port: Number(dockerApiAddressSplited[1]),
|
||||
}
|
||||
: {
|
||||
host: `${dockerApiAddressSplited[0]}:${dockerApiAddressSplited[1]}`,
|
||||
port: Number(dockerApiAddressSplited[2]),
|
||||
}
|
||||
|
||||
connectionParams.version = CaptainConstants.configs.dockerApiVersion
|
||||
const dockerApiInstance = new DockerApi(connectionParams)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Response } from 'express'
|
||||
import { UserInjected } from '../models/InjectionInterfaces'
|
||||
import { UserManager } from '../user/UserManager'
|
||||
import { IAppDef } from '../models/AppDefinition'
|
||||
|
||||
class InjectionExtractor {
|
||||
static extractUserFromInjected(res: Response) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UserManagerProvider } from '../user/UserManagerProvider'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import Logger from '../utils/Logger'
|
||||
import InjectionExtractor from './InjectionExtractor'
|
||||
import { IAppDef } from '../models/AppDefinition'
|
||||
|
||||
const dockerApi = DockerApiProvider.get()
|
||||
|
||||
|
||||
+17
-14
@@ -1,18 +1,20 @@
|
||||
type IAllAppDefinitions = IHashMapGeneric<IAppDef>
|
||||
import { IHashMapGeneric } from './ICacheGeneric'
|
||||
|
||||
interface IAppEnvVar {
|
||||
export type IAllAppDefinitions = IHashMapGeneric<IAppDef>
|
||||
|
||||
export interface IAppEnvVar {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface IAppVolume {
|
||||
export interface IAppVolume {
|
||||
containerPath: string
|
||||
volumeName?: string
|
||||
hostPath?: string
|
||||
mode?: string
|
||||
}
|
||||
|
||||
interface IAppPort {
|
||||
export interface IAppPort {
|
||||
containerPort: number
|
||||
hostPort: number
|
||||
protocol?: 'udp' | 'tcp'
|
||||
@@ -20,7 +22,7 @@ interface IAppPort {
|
||||
publishMode?: 'ingress' | 'host'
|
||||
}
|
||||
|
||||
interface RepoInfo {
|
||||
export interface RepoInfo {
|
||||
repo: string
|
||||
branch: string
|
||||
user: string
|
||||
@@ -28,7 +30,7 @@ interface RepoInfo {
|
||||
password: string
|
||||
}
|
||||
|
||||
interface RepoInfoEncrypted {
|
||||
export interface RepoInfoEncrypted {
|
||||
repo: string
|
||||
branch: string
|
||||
user: string
|
||||
@@ -36,23 +38,24 @@ interface RepoInfoEncrypted {
|
||||
passwordEncrypted: string
|
||||
}
|
||||
|
||||
interface IAppVersion {
|
||||
export interface IAppVersion {
|
||||
version: number
|
||||
deployedImageName?: string // empty if the deploy is not completed
|
||||
timeStamp: string
|
||||
gitHash: string | undefined
|
||||
}
|
||||
|
||||
interface IAppCustomDomain {
|
||||
export interface IAppCustomDomain {
|
||||
publicDomain: string
|
||||
hasSsl: boolean
|
||||
}
|
||||
|
||||
interface IAppTag {
|
||||
export interface IAppTag {
|
||||
tagName: string
|
||||
}
|
||||
|
||||
interface IAppDefinitionBase {
|
||||
export interface IAppDefinitionBase {
|
||||
projectId?: string | undefined
|
||||
description: string
|
||||
deployedVersion: number
|
||||
notExposeAsWebApp: boolean
|
||||
@@ -78,18 +81,18 @@ interface IAppDefinitionBase {
|
||||
appDeployTokenConfig?: AppDeployTokenConfig
|
||||
}
|
||||
|
||||
interface IHttpAuth {
|
||||
export interface IHttpAuth {
|
||||
user: string
|
||||
password?: string
|
||||
passwordHashed?: string
|
||||
}
|
||||
|
||||
interface AppDeployTokenConfig {
|
||||
export interface AppDeployTokenConfig {
|
||||
enabled: boolean
|
||||
appDeployToken?: string
|
||||
}
|
||||
|
||||
interface IAppDef extends IAppDefinitionBase {
|
||||
export interface IAppDef extends IAppDefinitionBase {
|
||||
appPushWebhook?: {
|
||||
tokenVersion: string
|
||||
repoInfo: RepoInfo
|
||||
@@ -100,7 +103,7 @@ interface IAppDef extends IAppDefinitionBase {
|
||||
isAppBuilding?: boolean
|
||||
}
|
||||
|
||||
interface IAppDefSaved extends IAppDefinitionBase {
|
||||
export interface IAppDefSaved extends IAppDefinitionBase {
|
||||
appPushWebhook:
|
||||
| {
|
||||
tokenVersion: string
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface IAutomatedCleanupConfigs {
|
||||
}
|
||||
|
||||
export class AutomatedCleanupConfigsCleaner {
|
||||
static cleanup(instance: IAutomatedCleanupConfigs) {
|
||||
static sanitizeInput(instance: IAutomatedCleanupConfigs) {
|
||||
return {
|
||||
mostRecentLimit:
|
||||
Number(instance.mostRecentLimit) > 0
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServerDockerInfo } from './ServerDockerInfo'
|
||||
|
||||
export interface RestoringNode {
|
||||
oldIp: string
|
||||
newIp: string
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface CapRoverExtraTheme {
|
||||
siderTheme?: string
|
||||
}
|
||||
|
||||
export default interface CapRoverTheme {
|
||||
content: string
|
||||
name: string
|
||||
extra?: string
|
||||
headEmbed?: string
|
||||
builtIn?: boolean
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
interface DockerAuthObj {
|
||||
export interface DockerAuthObj {
|
||||
serveraddress: string
|
||||
username: string
|
||||
password: string
|
||||
@@ -17,7 +17,7 @@ interface DockerAuthObj {
|
||||
}
|
||||
}
|
||||
*/
|
||||
interface DockerRegistryConfig {
|
||||
export interface DockerRegistryConfig {
|
||||
[serveraddress: string]: {
|
||||
username: string
|
||||
password: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface DockerSecret {
|
||||
export interface DockerSecret {
|
||||
secretName: string
|
||||
secretId: string
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
interface IHashMapGeneric<T> {
|
||||
export interface IHashMapGeneric<T> {
|
||||
[id: string]: T
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface ICaptainDefinition {
|
||||
export interface ICaptainDefinition {
|
||||
schemaVersion: number
|
||||
dockerfileLines?: string[]
|
||||
dockerfilePath?: string
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
interface IImageSource {
|
||||
import { RepoInfo } from './AppDefinition'
|
||||
|
||||
export interface IImageSource {
|
||||
uploadedTarPathSource?: { uploadedTarPath: string; gitHash: string }
|
||||
captainDefinitionContentSource?: {
|
||||
captainDefinitionContent: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface IServerBlockDetails {
|
||||
export interface IServerBlockDetails {
|
||||
hasSsl: boolean
|
||||
forceSsl: boolean
|
||||
websocketSupport: boolean
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class NetDataInfo {
|
||||
export class NetDataInfo {
|
||||
public isEnabled: boolean
|
||||
public data: {
|
||||
smtp: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { IAppDef } from './AppDefinition'
|
||||
|
||||
export type CaptainError = {
|
||||
captainErrorType: number
|
||||
apiMessage: string
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ProjectDefinition {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
parentProjectId?: string
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
interface ServerDockerInfo {
|
||||
export interface ServerDockerInfo {
|
||||
nodeId: string
|
||||
type: 'manager' | 'worker'
|
||||
isLeader: boolean
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import express = require('express')
|
||||
import ApiStatusCodes from '../../api/ApiStatusCodes'
|
||||
import BaseApi from '../../api/BaseApi'
|
||||
import { ThemeManagerPublic } from '../../user/ThemeManager'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/current', function (req, res, next) {
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManagerPublic().getCurrentTheme()
|
||||
})
|
||||
.then(function (t) {
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Current theme is retrieved.'
|
||||
)
|
||||
baseApi.data = {
|
||||
theme: t,
|
||||
}
|
||||
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,133 @@
|
||||
import express = require('express')
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import ApiStatusCodes from '../../api/ApiStatusCodes'
|
||||
import BaseApi from '../../api/BaseApi'
|
||||
import InjectionExtractor from '../../injection/InjectionExtractor'
|
||||
import { ProjectDefinition } from '../../models/ProjectDefinition'
|
||||
import Logger from '../../utils/Logger'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/register/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
const projectName = `${req.body.name || ''}`.trim()
|
||||
const parentProjectId = `${req.body.parentProjectId || ''}`.trim()
|
||||
const description = `${req.body.description || ''}`.trim()
|
||||
|
||||
Promise.resolve()
|
||||
.then(function () {
|
||||
const projectId = uuid()
|
||||
return dataStore.getProjectsDataStore().saveProject(projectId, {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
parentProjectId: parentProjectId,
|
||||
description: description,
|
||||
})
|
||||
})
|
||||
.then(function (project) {
|
||||
Logger.d(`Project created: ${projectName}`)
|
||||
const resp = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
`Project created: ${projectName}`
|
||||
)
|
||||
resp.data = project
|
||||
res.send(resp)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/delete/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
const projectIds = (req.body.projectIds || []).map((id: string) =>
|
||||
`${id}`.trim()
|
||||
)
|
||||
|
||||
Promise.resolve()
|
||||
.then(function () {
|
||||
return dataStore //
|
||||
.getProjectsDataStore()
|
||||
.deleteProjects(projectIds)
|
||||
})
|
||||
.then(function () {
|
||||
Logger.d(`Projects are deleted: ${projectIds}`)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, 'Project deleted'))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/update/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
const projectDefinition = req.body.projectDefinition as
|
||||
| ProjectDefinition
|
||||
| undefined
|
||||
|
||||
Promise.resolve()
|
||||
.then(function () {
|
||||
if (!projectDefinition) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project Definition is not provided'
|
||||
)
|
||||
}
|
||||
|
||||
projectDefinition.id = `${projectDefinition.id || ''}`
|
||||
projectDefinition.name = `${projectDefinition.name || ''}`
|
||||
projectDefinition.parentProjectId = `${
|
||||
projectDefinition.parentProjectId || ''
|
||||
}`
|
||||
projectDefinition.description = `${
|
||||
projectDefinition.description || ''
|
||||
}`
|
||||
|
||||
if (!projectDefinition.id) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_OPERATION,
|
||||
'Project ID is not provided'
|
||||
)
|
||||
}
|
||||
|
||||
return dataStore
|
||||
.getProjectsDataStore()
|
||||
.saveProject(projectDefinition.id, {
|
||||
id: projectDefinition.id,
|
||||
name: projectDefinition.name,
|
||||
parentProjectId: projectDefinition.parentProjectId,
|
||||
description: projectDefinition.description,
|
||||
})
|
||||
})
|
||||
.then(function () {
|
||||
Logger.d(`Project is saved: ${projectDefinition?.name}`)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, 'Project Saved'))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
// Get All Projects
|
||||
router.get('/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
dataStore
|
||||
.getProjectsDataStore()
|
||||
.getAllProjects()
|
||||
.then(function (projects) {
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Projects are retrieved.'
|
||||
)
|
||||
baseApi.data = {
|
||||
projects: projects,
|
||||
}
|
||||
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -9,9 +9,11 @@ import Utils from '../../utils/Utils'
|
||||
import AppsRouter from './apps/AppsRouter'
|
||||
import OneClickAppRouter from './oneclick/OneClickAppRouter'
|
||||
import ProRouter from './pro/ProRouter'
|
||||
import ProjectsRouter from './ProjectsRouter'
|
||||
import RegistriesRouter from './registeries/RegistriesRouter'
|
||||
import SystemRouter from './system/SystemRouter'
|
||||
import onFinished = require('on-finished')
|
||||
import { IHashMapGeneric } from '../../models/ICacheGeneric'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -124,6 +126,8 @@ router.post('/changepassword/', function (req, res, next) {
|
||||
|
||||
router.use('/apps/', AppsRouter)
|
||||
|
||||
router.use('/projects/', ProjectsRouter)
|
||||
|
||||
router.use('/oneclick/', OneClickAppRouter)
|
||||
|
||||
router.use('/registries/', RegistriesRouter)
|
||||
|
||||
@@ -7,6 +7,7 @@ import CaptainManager from '../../../../user/system/CaptainManager'
|
||||
import CaptainConstants from '../../../../utils/CaptainConstants'
|
||||
import Logger from '../../../../utils/Logger'
|
||||
import Utils from '../../../../utils/Utils'
|
||||
import { IAppDef, AppDeployTokenConfig } from '../../../../models/AppDefinition'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -191,6 +192,7 @@ router.post('/register/', function (req, res, next) {
|
||||
InjectionExtractor.extractUserFromInjected(res).user.serviceManager
|
||||
|
||||
const appName = req.body.appName as string
|
||||
const projectId = `${req.body.projectId || ''}`
|
||||
const hasPersistentData = !!req.body.hasPersistentData
|
||||
const isDetachedBuild = !!req.query.detached
|
||||
|
||||
@@ -198,9 +200,18 @@ router.post('/register/', function (req, res, next) {
|
||||
|
||||
Logger.d(`Registering app started: ${appName}`)
|
||||
|
||||
dataStore
|
||||
.getAppsDataStore()
|
||||
.registerAppDefinition(appName, hasPersistentData)
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
if (projectId) {
|
||||
return dataStore.getProjectsDataStore().getProject(projectId)
|
||||
// if project is not found, it will throw an error
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
return dataStore
|
||||
.getAppsDataStore()
|
||||
.registerAppDefinition(appName, projectId, hasPersistentData)
|
||||
})
|
||||
.then(function () {
|
||||
appCreated = true
|
||||
})
|
||||
@@ -322,6 +333,7 @@ router.post('/update/', function (req, res, next) {
|
||||
InjectionExtractor.extractUserFromInjected(res).user.serviceManager
|
||||
|
||||
const appName = req.body.appName
|
||||
const projectId = req.body.projectId
|
||||
const nodeId = req.body.nodeId
|
||||
const captainDefinitionRelativeFilePath =
|
||||
req.body.captainDefinitionRelativeFilePath
|
||||
@@ -384,13 +396,28 @@ router.post('/update/', function (req, res, next) {
|
||||
) {
|
||||
res.send(
|
||||
new BaseApi(
|
||||
ApiStatusCodes.STATUS_ERROR_GENERIC,
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Missing required Github/BitBucket/Gitlab field'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
repoInfo &&
|
||||
repoInfo.sshKey &&
|
||||
repoInfo.sshKey.indexOf('ENCRYPTED') > 0 &&
|
||||
!CaptainConstants.configs.disableEncryptedCheck
|
||||
) {
|
||||
res.send(
|
||||
new BaseApi(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'You cannot use encrypted SSH keys'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
repoInfo &&
|
||||
repoInfo.sshKey &&
|
||||
@@ -405,6 +432,7 @@ router.post('/update/', function (req, res, next) {
|
||||
serviceManager
|
||||
.updateAppDefinition(
|
||||
appName,
|
||||
projectId,
|
||||
description,
|
||||
Number(instanceCount),
|
||||
captainDefinitionRelativeFilePath,
|
||||
|
||||
@@ -12,10 +12,12 @@ import CaptainConstants from '../../../utils/CaptainConstants'
|
||||
import Logger from '../../../utils/Logger'
|
||||
import Utils from '../../../utils/Utils'
|
||||
import SystemRouteSelfHostRegistry from './selfhostregistry/SystemRouteSelfHostRegistry'
|
||||
import ThemesRouter from './ThemesRouter'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/selfhostregistry/', SystemRouteSelfHostRegistry)
|
||||
router.use('/themes/', ThemesRouter)
|
||||
|
||||
router.post('/createbackup/', function (req, res, next) {
|
||||
const backupManager = CaptainManager.get().getBackupManager()
|
||||
@@ -221,13 +223,13 @@ router.get('/diskcleanup/', function (req, res, next) {
|
||||
})
|
||||
|
||||
router.post('/diskcleanup/', function (req, res, next) {
|
||||
const configs = AutomatedCleanupConfigsCleaner.cleanup({
|
||||
mostRecentLimit: req.body.mostRecentLimit,
|
||||
cronSchedule: req.body.cronSchedule,
|
||||
timezone: req.body.timezone,
|
||||
})
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
const configs = AutomatedCleanupConfigsCleaner.sanitizeInput({
|
||||
mostRecentLimit: req.body.mostRecentLimit,
|
||||
cronSchedule: req.body.cronSchedule,
|
||||
timezone: req.body.timezone,
|
||||
})
|
||||
return CaptainManager.get()
|
||||
.getDiskCleanupManager()
|
||||
.setConfig(configs)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import express = require('express')
|
||||
import ApiStatusCodes from '../../../api/ApiStatusCodes'
|
||||
import BaseApi from '../../../api/BaseApi'
|
||||
import InjectionExtractor from '../../../injection/InjectionExtractor'
|
||||
import CapRoverTheme from '../../../models/CapRoverTheme'
|
||||
import { ThemeManager } from '../../../user/ThemeManager'
|
||||
import Logger from '../../../utils/Logger'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/setcurrent/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
const themeName = req.body.themeName || ''
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).setCurrent(themeName)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Current theme is stored.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/update/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
const oldName = req.body.oldName || ''
|
||||
const theme: CapRoverTheme = {
|
||||
name: req.body.name || '',
|
||||
content: req.body.content || '',
|
||||
extra: req.body.extra || '',
|
||||
headEmbed: req.body.headEmbed || '',
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).updateTheme(oldName, theme)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Theme is stored.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.post('/delete/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
const themeName = req.body.themeName || ''
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).deleteTheme(themeName)
|
||||
})
|
||||
.then(function () {
|
||||
const msg = 'Theme is deleted.'
|
||||
Logger.d(msg)
|
||||
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, msg))
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
router.get('/all/', function (req, res, next) {
|
||||
const dataStore =
|
||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return new ThemeManager(dataStore).getAllThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
const baseApi = new BaseApi(
|
||||
ApiStatusCodes.STATUS_OK,
|
||||
'Themes are retrieved.'
|
||||
)
|
||||
baseApi.data = {
|
||||
themes: themes,
|
||||
}
|
||||
|
||||
res.send(baseApi)
|
||||
})
|
||||
.catch(ApiStatusCodes.createCatcher(res))
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -6,6 +6,7 @@ import CaptainConstants from '../utils/CaptainConstants'
|
||||
import EnvVar from '../utils/EnvVars'
|
||||
import Logger from '../utils/Logger'
|
||||
import bcrypt = require('bcryptjs')
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
|
||||
const captainDefaultPassword = EnvVar.DEFAULT_PASSWORD || 'captain42'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import DataStore from '../datastore/DataStore'
|
||||
import RegistriesDataStore from '../datastore/RegistriesDataStore'
|
||||
import DockerApi from '../docker/DockerApi'
|
||||
import { DockerAuthObj, DockerRegistryConfig } from '../models/DockerAuthObj'
|
||||
import {
|
||||
IRegistryInfo,
|
||||
IRegistryType,
|
||||
@@ -14,7 +15,10 @@ import BuildLog from './BuildLog'
|
||||
|
||||
class DockerRegistryHelper {
|
||||
private registriesDataStore: RegistriesDataStore
|
||||
constructor(dataStore: DataStore, private dockerApi: DockerApi) {
|
||||
constructor(
|
||||
dataStore: DataStore,
|
||||
private dockerApi: DockerApi
|
||||
) {
|
||||
this.registriesDataStore = dataStore.getRegistriesDataStore()
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,12 @@ export default class FeatureFlags {
|
||||
Logger.e(error)
|
||||
})
|
||||
.then(function () {
|
||||
setTimeout(() => {
|
||||
self.refreshFeatureFlags()
|
||||
}, 1000 * 3600 * 19.3) // some random hour to avoid constant traffic
|
||||
setTimeout(
|
||||
() => {
|
||||
self.refreshFeatureFlags()
|
||||
},
|
||||
1000 * 3600 * 19.3
|
||||
) // some random hour to avoid constant traffic
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,11 @@ import tar = require('tar')
|
||||
import path = require('path')
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import DockerApi from '../docker/DockerApi'
|
||||
import { IAppEnvVar } from '../models/AppDefinition'
|
||||
import { IBuiltImage } from '../models/IBuiltImage'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import { ICaptainDefinition } from '../models/ICaptainDefinition'
|
||||
import { IImageSource } from '../models/IImageSource'
|
||||
import { AnyError } from '../models/OtherTypes'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import GitHelper from '../utils/GitHelper'
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import DataStore from '../datastore/DataStore'
|
||||
import DockerApi, { IDockerUpdateOrders } from '../docker/DockerApi'
|
||||
import {
|
||||
AppDeployTokenConfig,
|
||||
IAppDef,
|
||||
IAppEnvVar,
|
||||
IAppPort,
|
||||
IAppTag,
|
||||
IAppVolume,
|
||||
IHttpAuth,
|
||||
RepoInfo,
|
||||
} from '../models/AppDefinition'
|
||||
import { DockerAuthObj } from '../models/DockerAuthObj'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import { IImageSource } from '../models/IImageSource'
|
||||
import { PreDeployFunction } from '../models/OtherTypes'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import Logger from '../utils/Logger'
|
||||
@@ -587,6 +600,7 @@ class ServiceManager {
|
||||
|
||||
updateAppDefinition(
|
||||
appName: string,
|
||||
projectId: string,
|
||||
description: string,
|
||||
instanceCount: number,
|
||||
captainDefinitionRelativeFilePath: string,
|
||||
@@ -631,6 +645,16 @@ class ServiceManager {
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
projectId = `${projectId || ''}`.trim()
|
||||
if (projectId) {
|
||||
return dataStore
|
||||
.getProjectsDataStore()
|
||||
.getProject(projectId)
|
||||
|
||||
// if project is not found, it will throw an error
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
return self.ensureNotBuilding(appName)
|
||||
})
|
||||
@@ -715,6 +739,7 @@ class ServiceManager {
|
||||
.getAppsDataStore()
|
||||
.updateAppDefinitionInDb(
|
||||
appName,
|
||||
projectId,
|
||||
description,
|
||||
instanceCount,
|
||||
captainDefinitionRelativeFilePath,
|
||||
@@ -755,6 +780,7 @@ class ServiceManager {
|
||||
.getAppsDataStore()
|
||||
.updateAppDefinitionInDb(
|
||||
appName,
|
||||
existingAppDefinition.projectId,
|
||||
existingAppDefinition.description,
|
||||
existingAppDefinition.instanceCount,
|
||||
existingAppDefinition.captainDefinitionRelativeFilePath,
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import DataStore from '../datastore/DataStore'
|
||||
import DataStoreProvider from '../datastore/DataStoreProvider'
|
||||
import CapRoverTheme from '../models/CapRoverTheme'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import Logger from '../utils/Logger'
|
||||
import Utils from '../utils/Utils'
|
||||
import fs = require('fs-extra')
|
||||
|
||||
const builtInThemes = [] as CapRoverTheme[]
|
||||
|
||||
/**
|
||||
* Parses a string containing themed configuration fields into a JSON object.
|
||||
* Each field must start with "###CapRoverTheme." followed by the field name and its content.
|
||||
* The function dynamically identifies and extracts these fields, preserving their original formatting.
|
||||
* Field names are converted to lowercase to serve as keys in the resulting JSON object,
|
||||
* with the corresponding content as the values, maintaining any internal formatting.
|
||||
*
|
||||
* @param {string} input - Themed configuration string.
|
||||
* @return {Object} JSON object with keys representing field names and values containing the respective content.
|
||||
*
|
||||
* Example:
|
||||
* Input:
|
||||
* "###CapRoverTheme.name:
|
||||
* Green Arrow
|
||||
* ###CapRoverTheme.content:
|
||||
* { colorA: '#fff',
|
||||
* colorB: '#fff'
|
||||
* }"
|
||||
* Output:
|
||||
* { name: "Green Arrow", content: "{ colorA: '#fff' , colorB: '#fff' }" }
|
||||
*/
|
||||
|
||||
function parseCapRoverTheme(input: string) {
|
||||
const result = {} as { [id: string]: string }
|
||||
const lines = input.split('\n')
|
||||
let currentField = undefined as string | undefined
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.startsWith('###CapRoverTheme.')) {
|
||||
// Calculate the start position for the field name and remove the prefix '###CapRoverTheme.'
|
||||
const start = line.indexOf('.') + 1
|
||||
currentField = line.substring(start, line.length - 1).trim()
|
||||
result[currentField] = ''
|
||||
} else if (currentField) {
|
||||
// Check if we already have content for the current field to add a newline
|
||||
if (result[currentField].length > 0) {
|
||||
result[currentField] += '\n' + line
|
||||
} else {
|
||||
result[currentField] += line
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const key in result) {
|
||||
result[key] = result[key].trim()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function populateBuiltInThemes() {
|
||||
const themesDirectory = __dirname + '/../../template/themes'
|
||||
const rawContent = [] as string[]
|
||||
|
||||
const files = fs.readdirSync(themesDirectory).map((it) => {
|
||||
return {
|
||||
fileName: it,
|
||||
number: parseInt(it.split('-')[0]),
|
||||
}
|
||||
})
|
||||
|
||||
files.sort((a, b) => a.number - b.number)
|
||||
|
||||
files
|
||||
.map((it) => it.fileName)
|
||||
.forEach((file) => {
|
||||
const fileContent = fs.readFileSync(
|
||||
`${themesDirectory}/${file}`,
|
||||
'utf8'
|
||||
)
|
||||
rawContent.push(fileContent)
|
||||
})
|
||||
|
||||
rawContent.forEach((it) => {
|
||||
const parsedTheme = {
|
||||
...parseCapRoverTheme(it),
|
||||
builtIn: true,
|
||||
} as CapRoverTheme
|
||||
builtInThemes.push(parsedTheme)
|
||||
})
|
||||
}
|
||||
|
||||
populateBuiltInThemes()
|
||||
|
||||
export class ThemeManager {
|
||||
constructor(private dataStore: DataStore) {}
|
||||
|
||||
getAllThemes() {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return self.dataStore.getThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
return [...builtInThemes, ...themes]
|
||||
})
|
||||
}
|
||||
|
||||
deleteTheme(themeName: string) {
|
||||
const self = this
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return Promise.all([
|
||||
self.getAllThemes(),
|
||||
self.dataStore.getCurrentThemeName(),
|
||||
])
|
||||
})
|
||||
.then(function ([themesFetched, currentTheme]) {
|
||||
const themes = Utils.copyObject(themesFetched)
|
||||
const newThemes = [] as CapRoverTheme[]
|
||||
themes.forEach((it) => {
|
||||
if (it.name !== themeName) {
|
||||
newThemes.push(it)
|
||||
} else if (it.builtIn) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Cannot delete a built-in theme'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (themes.length === newThemes.length) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
if (currentTheme && currentTheme === themeName) {
|
||||
return self.dataStore.setCurrentTheme('')
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
return self.dataStore.deleteTheme(themeName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateTheme(oldName: string, theme: CapRoverTheme) {
|
||||
const self = this
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
theme.builtIn = false
|
||||
return self.getAllThemes()
|
||||
})
|
||||
.then(function (themesFetched) {
|
||||
const themes = Utils.copyObject(themesFetched)
|
||||
const idx = themes.findIndex((t) => t.name === oldName)
|
||||
if (!oldName) {
|
||||
// new theme
|
||||
|
||||
if (themes.some((t) => t.name === theme.name)) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Wanted to store a new theme, but it already exists with the same name'
|
||||
)
|
||||
}
|
||||
|
||||
themes.push(theme)
|
||||
} else if (idx >= 0) {
|
||||
// replacing existing theme
|
||||
|
||||
if (themes[idx].builtIn) {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Cannot edit a built-in theme'
|
||||
)
|
||||
}
|
||||
|
||||
themes[idx] = theme
|
||||
} else {
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
}
|
||||
|
||||
return self.dataStore
|
||||
.saveThemes(themes) //
|
||||
.then(() => {
|
||||
return self.dataStore.setCurrentTheme(theme.name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setCurrent(themeName: string) {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return self.getAllThemes()
|
||||
})
|
||||
.then(function (themes) {
|
||||
if (!themeName || themes.some((it) => it.name === themeName))
|
||||
return self.dataStore.setCurrentTheme(themeName)
|
||||
|
||||
throw ApiStatusCodes.createError(
|
||||
ApiStatusCodes.ILLEGAL_PARAMETER,
|
||||
'Theme not found'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
getCurrentTheme(): Promise<CapRoverTheme | undefined> {
|
||||
const self = this
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function () {
|
||||
return Promise.all([
|
||||
self.getAllThemes(),
|
||||
self.dataStore.getCurrentThemeName(),
|
||||
])
|
||||
})
|
||||
.then(function ([themes, themeName]) {
|
||||
if (!themeName) return undefined
|
||||
|
||||
const theme = themes.find((it) => it.name === themeName)
|
||||
|
||||
if (!theme) {
|
||||
Logger.e(
|
||||
new Error(
|
||||
'Theme name was provided but could not be found: ' +
|
||||
themeName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return theme
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeManagerPublic {
|
||||
private static themeManagerPublic = new ThemeManager(
|
||||
DataStoreProvider.getDataStore(CaptainConstants.rootNameSpace)
|
||||
)
|
||||
|
||||
getCurrentTheme() {
|
||||
return Promise.resolve() //
|
||||
.then(function () {
|
||||
return ThemeManagerPublic.themeManagerPublic.getCurrentTheme()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import CaptainConstants from '../utils/CaptainConstants'
|
||||
import { UserManager } from './UserManager'
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import { TwoFactorAuthResponse } from '../../models/IProFeatures'
|
||||
import ProManager from './ProManager'
|
||||
|
||||
export default class OtpAuthenticator {
|
||||
constructor(private dataStore: DataStore, private proManager: ProManager) {}
|
||||
constructor(
|
||||
private dataStore: DataStore,
|
||||
private proManager: ProManager
|
||||
) {}
|
||||
|
||||
set2fa(
|
||||
doEnable: boolean,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import SshClientImport = require('ssh2')
|
||||
import { exec } from 'child_process'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as moment from 'moment'
|
||||
import moment from 'moment'
|
||||
import * as path from 'path'
|
||||
import * as tar from 'tar'
|
||||
import ApiStatusCodes from '../../api/ApiStatusCodes'
|
||||
import DockerApi from '../../docker/DockerApi'
|
||||
import DockerUtils from '../../docker/DockerUtils'
|
||||
import { IAppDefSaved } from '../../models/AppDefinition'
|
||||
import { BackupMeta, RestoringInfo } from '../../models/BackupMeta'
|
||||
import { IHashMapGeneric } from '../../models/ICacheGeneric'
|
||||
import { ServerDockerInfo } from '../../models/ServerDockerInfo'
|
||||
import CaptainConstants from '../../utils/CaptainConstants'
|
||||
import Logger from '../../utils/Logger'
|
||||
import Utils from '../../utils/Utils'
|
||||
@@ -717,13 +720,16 @@ export default class BackupManager {
|
||||
)}-${now.valueOf()}`}${`-ip-${mainIP}.tar`}`
|
||||
fs.moveSync(tarFilePath, newName)
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
fs.removeSync(newName)
|
||||
} catch (err) {
|
||||
// nom nom
|
||||
}
|
||||
}, 1000 * 3600 * 2)
|
||||
setTimeout(
|
||||
() => {
|
||||
try {
|
||||
fs.removeSync(newName)
|
||||
} catch (err) {
|
||||
// nom nom
|
||||
}
|
||||
},
|
||||
1000 * 3600 * 2
|
||||
)
|
||||
|
||||
return Authenticator.getAuthenticator(
|
||||
namespace
|
||||
|
||||
@@ -25,6 +25,7 @@ import LoadBalancerManager from './LoadBalancerManager'
|
||||
import SelfHostedDockerRegistry from './SelfHostedDockerRegistry'
|
||||
import request = require('request')
|
||||
import fs = require('fs-extra')
|
||||
import { NetDataInfo } from '../../models/NetDataInfo'
|
||||
|
||||
const DEBUG_SALT = 'THIS IS NOT A REAL CERTIFICATE'
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ import Logger from '../../utils/Logger'
|
||||
|
||||
export default class DiskCleanupManager {
|
||||
private job: CronJob | undefined
|
||||
constructor(private dataStore: DataStore, private dockerApi: DockerApi) {
|
||||
constructor(
|
||||
private dataStore: DataStore,
|
||||
private dockerApi: DockerApi
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { v4 as uuid } from 'uuid'
|
||||
import ApiStatusCodes from '../../api/ApiStatusCodes'
|
||||
import DataStore from '../../datastore/DataStore'
|
||||
import DockerApi from '../../docker/DockerApi'
|
||||
import { IServerBlockDetails } from '../../models/IServerBlockDetails'
|
||||
import LoadBalancerInfo from '../../models/LoadBalancerInfo'
|
||||
import { AnyError } from '../../models/OtherTypes'
|
||||
import CaptainConstants from '../../utils/CaptainConstants'
|
||||
@@ -850,12 +851,15 @@ class LoadBalancerManager {
|
||||
// this random schedule helps to avoid retrying at the same time of
|
||||
// the day in case if that's our super high traffic time
|
||||
|
||||
setTimeout(function () {
|
||||
self.renewAllCertsAndReload() //
|
||||
.catch((err) => {
|
||||
Logger.e(err)
|
||||
})
|
||||
}, 1000 * 3600 * 20.3)
|
||||
setTimeout(
|
||||
function () {
|
||||
self.renewAllCertsAndReload() //
|
||||
.catch((err) => {
|
||||
Logger.e(err)
|
||||
})
|
||||
},
|
||||
1000 * 3600 * 20.3
|
||||
)
|
||||
|
||||
return self.certbotManager
|
||||
.renewAllCerts() //
|
||||
|
||||
@@ -96,7 +96,6 @@ export default class ApacheMd5 {
|
||||
let final = crypto
|
||||
.createHash('md5')
|
||||
.update(password + salt + password, 'ascii')
|
||||
//@ts-ignore
|
||||
.digest(DIGEST_ENCODING)
|
||||
|
||||
for (let pl = password.length; pl > 0; pl -= 16) {
|
||||
@@ -111,11 +110,9 @@ export default class ApacheMd5 {
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
final = crypto
|
||||
.createHash('md5')
|
||||
.update(ctx, 'ascii')
|
||||
//@ts-ignore
|
||||
.digest(DIGEST_ENCODING)
|
||||
|
||||
// 1000 loop.
|
||||
@@ -147,7 +144,6 @@ export default class ApacheMd5 {
|
||||
final = crypto
|
||||
.createHash('md5')
|
||||
.update(ctxl, 'ascii')
|
||||
//@ts-ignore
|
||||
.digest(DIGEST_ENCODING)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const CONSTANT_FILE_OVERRIDE_USER =
|
||||
const configs = {
|
||||
publishedNameOnDockerHub: 'caprover/caprover',
|
||||
|
||||
version: '1.12.0',
|
||||
version: '1.13.0',
|
||||
|
||||
defaultMaxLogSize: '512m',
|
||||
|
||||
@@ -58,6 +58,9 @@ const configs = {
|
||||
certbotImageName: 'caprover/certbot-sleeping:v2.11.0',
|
||||
|
||||
certbotCertCommandRules: undefined as CertbotCertCommandRule[] | undefined,
|
||||
|
||||
// this is added in 1.13 just as a safety - remove this after 1.14
|
||||
disableEncryptedCheck: false,
|
||||
}
|
||||
|
||||
export interface CertbotCertCommandRule {
|
||||
@@ -205,7 +208,7 @@ function overrideConfigFromFile(fileName: string) {
|
||||
}
|
||||
|
||||
console.log(`Overriding ${prop} from ${fileName}`)
|
||||
// @ts-ignore
|
||||
// @ts-expect-error "this actually works"
|
||||
configs[prop] = overridingValuesConfigs[prop]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import externalIp = require('public-ip')
|
||||
import DockerApi from '../docker/DockerApi'
|
||||
import { IAppEnvVar, IAppPort } from '../models/AppDefinition'
|
||||
import BackupManager from '../user/system/BackupManager'
|
||||
import CaptainConstants from './CaptainConstants'
|
||||
import EnvVar from './EnvVars'
|
||||
@@ -198,6 +198,11 @@ function printTroubleShootingUrl() {
|
||||
|
||||
let myIp4: string
|
||||
|
||||
async function initializeExternalIp() {
|
||||
const externalIp = await import('public-ip')
|
||||
return externalIp
|
||||
}
|
||||
|
||||
export function install() {
|
||||
const backupManger = new BackupManager()
|
||||
|
||||
@@ -222,11 +227,26 @@ export function install() {
|
||||
return checkSystemReq()
|
||||
})
|
||||
.then(function () {
|
||||
return initializeExternalIp()
|
||||
})
|
||||
.then(function (externalIp) {
|
||||
if (EnvVar.MAIN_NODE_IP_ADDRESS) {
|
||||
return EnvVar.MAIN_NODE_IP_ADDRESS
|
||||
}
|
||||
|
||||
return externalIp.v4()
|
||||
try {
|
||||
const externalIpFetched = externalIp.publicIpv4()
|
||||
if (externalIpFetched) {
|
||||
return externalIpFetched
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Defaulting to 127.0.0.1 - Error retrieving IP address:',
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
return '127.0.0.1'
|
||||
})
|
||||
.then(function (ip4) {
|
||||
if (!ip4) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as childProcess from 'child_process'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as git from 'simple-git/promise'
|
||||
import git from 'simple-git'
|
||||
import * as util from 'util'
|
||||
import * as uuid from 'uuid'
|
||||
import CaptainConstants from './CaptainConstants'
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import * as moment from 'moment'
|
||||
import moment from 'moment'
|
||||
import { AnyError } from '../models/OtherTypes'
|
||||
import CaptainConstants from './CaptainConstants'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import DataStore from '../datastore/DataStore'
|
||||
import DockerApi from '../docker/DockerApi'
|
||||
import { IAppVersion } from '../models/AppDefinition'
|
||||
import { IRegistryTypes } from '../models/IRegistryInfo'
|
||||
import Authenticator from '../user/Authenticator'
|
||||
import CaptainConstants from './CaptainConstants'
|
||||
@@ -206,6 +207,7 @@ export default class MigrateCaptainDuckDuck {
|
||||
.then(function () {
|
||||
return appStore.registerAppDefinition(
|
||||
appName,
|
||||
'',
|
||||
!!app.hasPersistentData
|
||||
)
|
||||
})
|
||||
@@ -322,6 +324,7 @@ export default class MigrateCaptainDuckDuck {
|
||||
return appStore.updateAppDefinitionInDb(
|
||||
appName,
|
||||
'',
|
||||
'',
|
||||
Number(app.instanceCount),
|
||||
CaptainConstants.defaultCaptainDefinitionPath,
|
||||
app.envVars || [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import request = require('request')
|
||||
import ApiStatusCodes from '../api/ApiStatusCodes'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import { ITemplate } from '../models/OtherTypes'
|
||||
import Logger from './Logger'
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import * as yaml from 'yaml'
|
||||
import Logger from './Logger'
|
||||
|
||||
export default class Utils {
|
||||
static copyObject<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj)) as T
|
||||
}
|
||||
|
||||
static removeHttpHttps(input: string) {
|
||||
input = input.trim()
|
||||
input = input.replace(/^(?:http?:\/\/)?/i, '')
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
###CapRoverTheme.name:
|
||||
Legacy
|
||||
|
||||
|
||||
###CapRoverTheme.content:
|
||||
{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#1b8ad3',
|
||||
colorLink: '#1b8ad3',
|
||||
fontFamily: `QuickSand, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
###CapRoverTheme.headEmbed:
|
||||
<link href="https://fonts.googleapis.com/css?family=Quicksand:300,500" rel="stylesheet" />
|
||||
|
||||
|
||||
###CapRoverTheme.extra:
|
||||
{siderTheme:'dark'}
|
||||
@@ -0,0 +1,36 @@
|
||||
###CapRoverTheme.name:
|
||||
Teal Zen
|
||||
|
||||
|
||||
|
||||
###CapRoverTheme.content:
|
||||
{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
components: {
|
||||
Menu: {
|
||||
itemBg: isDarkMode ? '#151515' : '#fafafa',
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#1a362f',
|
||||
},
|
||||
},
|
||||
token: {
|
||||
colorPrimary: '#008264',
|
||||
colorLink: '#02bf94',
|
||||
fontFamily: `'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
borderRadius: 20,
|
||||
lineWidth: 2,
|
||||
colorBgLayout: '#dadada',
|
||||
},
|
||||
}
|
||||
|
||||
###CapRoverTheme.headEmbed:
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
###CapRoverTheme.extra:
|
||||
{}
|
||||
@@ -0,0 +1,58 @@
|
||||
###CapRoverTheme.name:
|
||||
Robotic
|
||||
|
||||
|
||||
|
||||
###CapRoverTheme.content:
|
||||
{
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
components: {
|
||||
Menu: {
|
||||
itemPadding: '0 20px',
|
||||
itemHeight: '40px',
|
||||
height: '56px',
|
||||
colorText: '#fff',
|
||||
colorBg: '#3f3f3f',
|
||||
colorSubBg: '#4f4f4f',
|
||||
colorItemBg: '#5f5f5f',
|
||||
colorItemBgActive: '#7f7bff',
|
||||
colorItemText: '#fff',
|
||||
colorItemTextActive: '#fff',
|
||||
colorItemIcon: '#fff',
|
||||
colorItemIconActive: '#fff',
|
||||
colorSider: '#3f3f3f',
|
||||
colorSiderTitle: '#fff',
|
||||
colorSiderTitleIcon: '#fff',
|
||||
colorSiderTrigger: '#fff',
|
||||
colorSiderTriggerIcon: '#fff',
|
||||
colorSiderTriggerIconActive: '#7f7bff',
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#3f3f3f',
|
||||
footerBg: '#4f4f4f',
|
||||
siderWidth: '256px',
|
||||
layoutBg: '#f0f2f5',
|
||||
contentBg: '#fff',
|
||||
colorPrimary: '#7f7bff',
|
||||
colorPrimaryBg: '#7f7bff',
|
||||
colorInfo: '#7f7bff',
|
||||
},
|
||||
},
|
||||
token: {
|
||||
borderRadius: 2,
|
||||
colorPrimary: '#7f7bff',
|
||||
colorLink: '#2672c9',
|
||||
fontFamily: `'Oxanium', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
###CapRoverTheme.headEmbed:
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@200..800&display=swap" rel="stylesheet">
|
||||
|
||||
###CapRoverTheme.extra:
|
||||
{}
|
||||
@@ -0,0 +1,145 @@
|
||||
import configstore from 'configstore'
|
||||
import AppsDataStore from '../src/datastore/AppsDataStore'
|
||||
import ProjectsDataStore from '../src/datastore/ProjectsDataStore'
|
||||
import { ProjectDefinition } from '../src/models/ProjectDefinition'
|
||||
|
||||
describe('ProjectsDataStore', () => {
|
||||
let projectsDataStore: ProjectsDataStore
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the configstore and AppsDataStore
|
||||
const mockConfigstore = new configstore('test')
|
||||
const mockAppsDataStore = {} as AppsDataStore
|
||||
projectsDataStore = new ProjectsDataStore(
|
||||
mockConfigstore,
|
||||
mockAppsDataStore
|
||||
)
|
||||
})
|
||||
|
||||
describe('organizeFromTheLeafsToRoot', () => {
|
||||
it('should correctly organize projects from leaves to root', () => {
|
||||
const input = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Root 1',
|
||||
parentProjectId: '',
|
||||
description: 'Root 1 desc',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Child 1',
|
||||
parentProjectId: '1',
|
||||
description: 'Child 1 desc',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Child 2',
|
||||
parentProjectId: '1',
|
||||
description: 'Child 2 desc',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Grandchild 1',
|
||||
parentProjectId: '2',
|
||||
description: 'Grandchild 1 desc',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Root 2',
|
||||
parentProjectId: '',
|
||||
description: 'Root 2 desc',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Child 3',
|
||||
parentProjectId: '5',
|
||||
description: 'Child 3 desc',
|
||||
},
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '4',
|
||||
name: 'Grandchild 1',
|
||||
parentProjectId: '2',
|
||||
description: 'Grandchild 1 desc',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Child 1',
|
||||
parentProjectId: '1',
|
||||
description: 'Child 1 desc',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Child 2',
|
||||
parentProjectId: '1',
|
||||
description: 'Child 2 desc',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
name: 'Root 1',
|
||||
parentProjectId: '',
|
||||
description: 'Root 1 desc',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Child 3',
|
||||
parentProjectId: '5',
|
||||
description: 'Child 3 desc',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Root 2',
|
||||
parentProjectId: '',
|
||||
description: 'Root 2 desc',
|
||||
},
|
||||
]
|
||||
|
||||
const result = projectsDataStore.organizeFromTheLeafsToRoot(input)
|
||||
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const input: ProjectDefinition[] = []
|
||||
const result = projectsDataStore.organizeFromTheLeafsToRoot(input)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle projects with no parent', () => {
|
||||
const input = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Root 1',
|
||||
parentProjectId: '',
|
||||
description: 'Root 1 desc',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Root 2',
|
||||
parentProjectId: '',
|
||||
description: 'Root 2 desc',
|
||||
},
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Root 1',
|
||||
parentProjectId: '',
|
||||
description: 'Root 1 desc',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Root 2',
|
||||
parentProjectId: '',
|
||||
description: 'Root 2 desc',
|
||||
},
|
||||
]
|
||||
|
||||
const result = projectsDataStore.organizeFromTheLeafsToRoot(input)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
+6
-7
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"strictNullChecks": true,
|
||||
"outDir": "./built",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"target": "es6"
|
||||
"target": "ES2018"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user