fix(core): update

This commit is contained in:
Philipp Kunz 2024-04-20 12:21:41 +02:00
commit c24262f765
84 changed files with 14340 additions and 0 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,71 @@
name: Docker (tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
security:
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci npm prepare
npmci node install stable
npmci npm install
npmci command npm run build

View File

@ -0,0 +1,106 @@
name: Docker (tags)
on:
push:
tags:
- '*'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
security:
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci command npm run build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: registry.gitlab.com/hosttoday/ht-docker-dbase:npmci
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
- name: Release
run: |
npmci docker login
npmci docker build
npmci docker test
# npmci docker push gitea.lossless.digital
npmci docker push dockerregistry.lossless.digital
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Trigger
run: npmci trigger

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
.nogit/
# artifacts
coverage/
public/
pages/
# installs
node_modules/
# caches
.yarn/
.cache/
.rpt2_cache
# builds
dist/
dist_*/
# custom

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"type": "node-terminal"
}
]
}

26
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"schema": {
"type": "object",
"properties": {
"npmci": {
"type": "object",
"description": "settings for npmci"
},
"gitzone": {
"type": "object",
"description": "settings for gitzone",
"properties": {
"projectType": {
"type": "string",
"enum": ["website", "element", "service", "npm", "wcc"]
}
}
}
}
}
}
]
}

46
Dockerfile Normal file
View File

@ -0,0 +1,46 @@
# gitzone dockerfile_service
## STAGE 1 // BUILD
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1
COPY ./ /app
WORKDIR /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN rm -rf node_modules && pnpm install
RUN pnpm run build
# gitzone dockerfile_service
## STAGE 2 // install production
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2
WORKDIR /app
COPY --from=node1 /app /app
RUN rm -rf .pnpm-store
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN rm -rf node_modules/ && pnpm install --prod
## STAGE 3 // rebuild dependencies for alpine
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3
WORKDIR /app
COPY --from=node2 /app /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN pnpm rebuild -r
## STAGE 4 // the final production image with all dependencies in place
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4
WORKDIR /app
COPY --from=node3 /app /app
### Healthchecks
RUN pnpm install -g @servezone/healthy
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
EXPOSE 80
CMD ["npm", "start"]

View File

4
cli.child.ts Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as cliTool from './ts/index.js';
cliTool.runCli();

4
cli.js Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
const cliTool = await import('./dist_ts/index.js');
cliTool.runCli();

5
cli.ts.js Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as tsrun from '@git.zone/tsrun';
tsrun.runPath('./cli.child.js', import.meta.url);

124
html/index.html Normal file
View File

@ -0,0 +1,124 @@
<!--gitzone default-->
<!-- made by Lossless GmbH -->
<!-- checkout https://maintainedby.lossless.com for awesome OpenSource projects -->
<!DOCTYPE html>
<html lang="en">
<head>
<!--Lets set some basic meta tags-->
<meta
name="viewport"
content="user-scalable=0, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="theme-color" content="#000000" />
<!--Lets make sure we recognize this as an PWA-->
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/png" href="/assetbroker/manifest/favicon.png" />
<!--Lets load standard fonts-->
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
<!--Lets avoid a rescaling flicker due to default body margins-->
<style>
html {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
position: relative;
background: #000;
margin: 0px;
overflow: hidden;
}
</style>
<script>
projectVersion = '';
</script>
</head>
<body>
<noscript>
<style>
body {
background: #303f9f;
font-family: Inter, Roboto, sans-serif;
color: #ffffff;
min-height: 100vh;
position: relative;
}
a {
color: #ffffff;
text-decoration: none;
}
.logo {
margin-top: 100px;
text-align: center;
}
img {
width: 130px;
}
.container {
width: 600px;
margin: auto;
margin-top: 20px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
overflow: hidden;
border-radius: 3px;
background: #4357d9;
}
.contentHeader {
padding: 20px;
text-align: center;
font-size: 25px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.content {
padding: 20px;
}
.footer {
padding: 10px;
text-align: center;
}
</style>
<div class="logo">
<img src="https://assetbroker.lossless.one/brandfiles/lossless/svg-minimal-bright.svg" />
</div>
<div class="container">
<div class="contentHeader">We need JavaScript to run properly!</div>
<div class="content">
This site is being built using lit-element (made by Google). This technology works with
JavaScript. Subsequently this website does not work as intended by Lossless GmbH without
JavaScript.
</div>
</div>
<div class="footer">
<a href="https://lossless.gmbh">Legal Info</a> |
<a href="https://lossless.gmbh/privacy">Privacy Policy</a>
</div>
</noscript>
<script type="text/javascript" async defer>
window.revenueEnabled = true;
const runRevenueCheck = async () => {
var e = document.createElement('div');
e.id = '476kjuhzgtr764';
e.style.display = 'none';
document.body.appendChild(e);
if (document.getElementById('476kjuhzgtr764')) {
window.revenueEnabled = true;
} else {
window.revenueEnabled = false;
}
console.log(`revenue enabled: ${window.revenueEnabled}`);
};
runRevenueCheck();
</script>
</body>
<script defer type="module" src="/bundle.js"></script>
</html>

40
npmextra.json Normal file
View File

@ -0,0 +1,40 @@
{
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public",
"npmRegistryUrl": "verdaccio.lossless.one",
"dockerRegistryRepoMap": {
"registry.gitlab.com": "losslessone/services/servezone/cloudly"
},
"dockerBuildargEnvMap": {
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
}
},
"gitzone": {
"projectType": "service",
"module": {
"githost": "gitlab.com",
"gitscope": "servezone/private",
"gitrepo": "cloudly",
"description": "A cloud manager utilizing Docker Swarmkit, designed for operations on Cloudron, and supports various cloud platforms like DigitalOcean, Hetzner Cloud, and Cloudflare.",
"npmPackagename": "@serve.zone/cloudly",
"license": "UNLICENSED",
"keywords": [
"cloud",
"Docker Swarmkit",
"DigitalOcean",
"Hetzner Cloud",
"Cloudflare",
"container management",
"configuration management",
"LetsEncrypt SSL",
"cloud infrastructure automation",
"node.js",
"TypeScript"
]
}
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}

102
package.json Normal file
View File

@ -0,0 +1,102 @@
{
"name": "@serve.zone/cloudly",
"version": "1.0.213",
"private": false,
"description": "A cloud manager utilizing Docker Swarmkit, designed for operations on Cloudron, and supports various cloud platforms like DigitalOcean, Hetzner Cloud, and Cloudflare.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"author": "Task Venture Capital GmbH",
"license": "UNLICENSED",
"scripts": {
"test": "(tstest test/)",
"build": "tsbuild --web --allowimplicitany && tsbundle website --production",
"start": "node cli.js",
"startTs": "node cli.ts.js",
"watch": "tswatch website",
"localPublish": "gitzone commit"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.65",
"@git.zone/tsbundle": "^2.0.15",
"@git.zone/tstest": "^1.0.73",
"@git.zone/tswatch": "^2.0.23",
"@push.rocks/tapbundle": "^5.0.4",
"@types/node": "^20.11.25"
},
"dependencies": {
"@api.global/typedrequest": "3.0.19",
"@api.global/typedserver": "^3.0.27",
"@api.global/typedsocket": "^3.0.0",
"@apiclient.xyz/cloudflare": "^6.0.1",
"@apiclient.xyz/digitalocean": "^1.0.5",
"@apiclient.xyz/hetznercloud": "^1.0.18",
"@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.0.289",
"@design.estate/dees-domtools": "^2.0.57",
"@design.estate/dees-element": "^2.0.34",
"@git.zone/tsrun": "^1.2.37",
"@push.rocks/early": "^4.0.3",
"@push.rocks/npmextra": "^5.0.10",
"@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.0.5",
"@push.rocks/smartacme": "^4.0.8",
"@push.rocks/smartbucket": "^2.0.4",
"@push.rocks/smartcli": "^4.0.6",
"@push.rocks/smartdata": "^5.0.8",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartexit": "^1.0.20",
"@push.rocks/smartfile": "^11.0.4",
"@push.rocks/smartguard": "^2.0.1",
"@push.rocks/smartjson": "^5.0.14",
"@push.rocks/smartjwt": "^2.0.4",
"@push.rocks/smartlog": "^3.0.1",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.11",
"@push.rocks/smartpath": "^5.0.5",
"@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartrequest": "^2.0.11",
"@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartunique": "^3.0.8",
"@push.rocks/taskbuffer": "^3.0.2",
"@push.rocks/webjwt": "^1.0.9",
"@serve.zone/interfaces": "^1.0.47",
"@tsclass/tsclass": "^4.0.52"
},
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
],
"browserslist": [
"last 1 chrome versions"
],
"repository": {
"type": "git",
"url": "git+https://gitlab.com/servezone/private/cloudly.git"
},
"bugs": {
"url": "https://gitlab.com/servezone/private/cloudly/issues"
},
"homepage": "https://gitlab.com/servezone/private/cloudly#readme",
"keywords": [
"cloud",
"Docker Swarmkit",
"DigitalOcean",
"Hetzner Cloud",
"Cloudflare",
"container management",
"configuration management",
"LetsEncrypt SSL",
"cloud infrastructure automation",
"node.js",
"TypeScript"
]
}

9227
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

12
qenv.yml Normal file
View File

@ -0,0 +1,12 @@
required:
- CF_TOKEN
- DIGITALOCEAN_TOKEN
- GITLAB_USER
- GITLAB_TOKEN
- LETSENCRYPT_EMAIL
- MONGODB_URL
- MONGODB_DATABASE
- MONGODB_PASSWORD
- SERVEZONE_URL
- SERVEZONE_PORT
- SERVEZONE_ENVIRONMENT

0
readme.hints.md Normal file
View File

104
readme.md Normal file
View File

@ -0,0 +1,104 @@
# @serve.zone/cloudly
configure the cloud
## Install
To install `@serve.zone/cloudly`, run the following command in your terminal:
```bash
npm install @serve.zone/cloudly --save
```
This will install the package and add it to your project's `package.json` dependencies.
## Usage
`@serve.zone/cloudly` is designed to help you manage and configure cloud environments. This package provides a comprehensive TypeScript and ESM-based interface for interacting with various cloud services, including Docker Swarmkit cluster management, and integration with cloud providers such as DigitalOcean, Hetzner Cloud, and Cloudflare.
### Getting Started
Before diving into the specifics, ensure your environment is properly set up. This includes having Node.js installed (preferably the latest LTS version), and if you are working in a TypeScript project, ensure TypeScript is configured.
#### Initializing Cloudly
First, import `Cloudly` class from the package and initialize it as shown below:
```typescript
import { Cloudly } from '@serve.zone/cloudly';
const myCloudlyInstance = new Cloudly();
```
The `Cloudly` class is the entry point to using the library features. It prepares the environment for configuring the cloud services.
#### Configuration
Configuration plays a pivotal role in how `@serve.zone/cloudly` operates. The library expects certain configurations to be provided, which can include credentials for cloud services, database connections, etc.
For example, to configure a connection to MongoDB, specify your MongoDB details as shown:
```typescript
const myCloudlyConfig = {
mongoDescriptor: {
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
mongoDbName: 'myDatabase',
mongoDbUser: 'myUser',
mongoDbPass: 'myPassword',
},
// Additional configuration values...
};
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
```
#### Managing Docker Swarmkit Cluster
Cloudly allows managing Docker Swarmkit clusters through an abstracted interface, simplifying operations such as deployment and scaling.
```typescript
// Assuming myCloudlyInstance is already configured and initialized
// Start the cloud instance
await myCloudlyInstance.start();
// Now you can perform various operations on your Docker Swarmkit cluster
```
### Integration with Cloud Providers
`@serve.zone/cloudly` integrates seamlessly with cloud providers like DigitalOcean, Hetzner Cloud, etc., by leveraging the power of APIs provided by these platforms.
#### Managing DigitalOcean Resources
To manage DigitalOcean resources, you'll need to configure your DigitalOcean token and then use the provided interfaces to interact with the resources, such as creating droplets, managing volumes, etc.
```typescript
// Set your DigitalOcean API token
const digitalOceanToken = "your_digital_ocean_api_token";
// Now you can use myCloudlyInstance to manage DigitalOcean resources
```
#### Using the Cloudflare Integration
Similarly, for managing DNS records and SSL certificates with Cloudflare, set up your Cloudflare API token:
```typescript
const cloudflareToken = "your_cloudflare_api_token";
// Use myCloudlyInstance to interact with Cloudflare, such as setting DNS records
```
### Advanced Usage
`@serve.zone/cloudly` offers more than just cloud resource management. It integrates various modules for error logging, security, working with JSON data, and much more. Explore the comprehensive documentation and typings to leverage the full potential of the package.
### Conclusion
With `@serve.zone/cloudly`, configuring the cloud becomes a less tedious task. By abstracting away the complexities and providing a unified interface to manage cloud resources, development efficiency is significantly improved. The examples provided above merely scratch the surface of what's possible. Dive into the detailed documentation to explore all features and capabilities.
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@ -0,0 +1,39 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
delete process.env.CLI_CALL;
import * as cloudly from '../ts/index.js';
process.env.TESTING_CLOUDLY = 'true';
let testCloudly: cloudly.Cloudly;
tap.test('first test', async () => {
const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: await testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: await testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: await testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
mongoDescriptor: {
mongoDbName: await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbPass: await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL')
},
digitalOceanToken: await testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
};
testCloudly = new cloudly.Cloudly(cloudlyConfig);
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
});
tap.test('should init servezone', async () => {
await testCloudly.start();
});
tap.test('should end the service', async () => {
await testCloudly.stop();
});
tap.start();

View File

@ -0,0 +1,39 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
delete process.env.CLI_CALL;
import * as cloudly from '../ts/index';
process.env.TESTING_CLOUDLY = 'true';
let testCloudly: cloudly.Cloudly;
tap.test('first test', async () => {
const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL')
},
digitalOceanToken: testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
};
testCloudly = new cloudly.Cloudly(cloudlyConfig);
expect(testCloudly).to.be.instanceof(cloudly.Cloudly);
});
tap.test('should init servezone', async () => {
await testCloudly.start();
});
tap.test('should end the service', async () => {
await testCloudly.stop();
});
tap.start();

39
test/test.ts Normal file
View File

@ -0,0 +1,39 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
process.env.TESTING_CLOUDLY = 'true';
delete process.env.CLI_CALL;
import * as cloudly from '../ts/index.js';
let testCloudly: cloudly.Cloudly;
tap.test('first test', async () => {
const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbUser: testQenv.getEnvVarOnDemand('MONGODB_USER'),
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL'),
},
digitalOceanToken: testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN'),
};
testCloudly = new cloudly.Cloudly(cloudlyConfig);
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
});
tap.test('should init cloudly', async () => {
await testCloudly.start();
});
tap.test('should end the service', async (tools) => {
await testCloudly.stop();
tools.delayFor(1000).then(() => process.exit())
});
tap.start();

1
test/test_latest.sh Normal file
View File

@ -0,0 +1 @@
echo 'hi'

8
ts/00_commitinfo_data.ts Normal file
View File

@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: '@serve.zone/cloudly',
version: '1.0.214',
description: 'A cloud manager utilizing Docker Swarmkit, designed for operations on Cloudron, and supports various cloud platforms like DigitalOcean, Hetzner Cloud, and Cloudflare.'
}

View File

@ -0,0 +1,120 @@
import * as plugins from './cloudly.plugins.js';
import { CloudlyConfig } from './cloudly.classes.config.js';
// interfaces
import {} from '@tsclass/tsclass';
// Cloudly mods
import { CloudlyInfo } from './cloudly.classes.cloudlyinfo.js';
import { CloudlyServer } from './cloudly.classes.server.js';
// connectors
import { CloudflareConnector } from './connector.cloudflare/connector.js';
import { LetsencryptConnector } from './connector.letsencrypt/connector.js';
import { MongodbConnector } from './connector.mongodb/connector.js';
// processes
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
import { ClusterManager } from './manager.cluster/clustermanager.js';
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
import { CloudlyVersionManager } from './manager.version/versionmanager.js';
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'
import { CloudlyServerManager } from './manager.server/servermanager.js';
import { ExternalApiManager } from './manager.status/statusmanager.js';
import { ImageManager } from './manager.image/classes.imagemanager.js';
import { logger } from './cloudly.logging.js';
/**
* Cloudly class can be used to instantiate a cloudly server.
* It is implemented as class in order to make it easier ro write node services that are more adjusted to invidual services
*
* ```ts
* const mycloudly = new Cloudly ({...})
* ```
*/
export class Cloudly {
public typedrouter = new plugins.typedrequest.TypedRouter();
public config: CloudlyConfig;
public logger: plugins.smartlog.Smartlog;
public ready: Promise<any>;
// mods
public cloudlyInfo: CloudlyInfo;
public server: CloudlyServer;
// connectors
public cloudflareConnector: CloudflareConnector;
public letsencryptConnector: LetsencryptConnector;
public mongodbConnector: MongodbConnector;
// managers
public secretManager: CloudlySecretManager;
public clusterManager: ClusterManager;
public coreflowManager: CloudlyCoreflowManager;
public externalApiManager: ExternalApiManager;
public imageManager: ImageManager;
public taskManager: CloudlyTaskmanager;
public versionManager: CloudlyVersionManager;
public serverManager: CloudlyServerManager;
private readyDeferred = new plugins.smartpromise.Deferred();
constructor() {
this.cloudlyInfo = new CloudlyInfo(this);
this.config = new CloudlyConfig(this);
this.logger = logger;
this.server = new CloudlyServer(this);
this.ready = this.readyDeferred.promise;
// connectors
this.mongodbConnector = new MongodbConnector(this); // database needs to go first
this.cloudflareConnector = new CloudflareConnector(this);
this.letsencryptConnector = new LetsencryptConnector(this);
// processes
this.clusterManager = new ClusterManager(this);
this.coreflowManager = new CloudlyCoreflowManager(this);
this.externalApiManager = new ExternalApiManager(this);
this.imageManager = new ImageManager(this);
this.taskManager = new CloudlyTaskmanager(this);
this.versionManager = new CloudlyVersionManager(this);
this.secretManager = new CloudlySecretManager(this);
this.serverManager = new CloudlyServerManager(this);
}
/**
* starts the cloudly instance and
* @param configArg
*/
public async start() {
// config
await this.config.init();
// manageers
await this.secretManager.start();
await this.serverManager.start();
// connectors
await this.mongodbConnector.init();
await this.cloudflareConnector.init();
await this.letsencryptConnector.init();
await this.clusterManager.init();
await this.server.start();
this.readyDeferred.resolve();
// start the managers
this.imageManager.start();
}
/**
* stop the reception instance
*/
public async stop() {
await this.server.stop();
await this.letsencryptConnector.stop();
await this.mongodbConnector.stop();
await this.secretManager.stop();
}
}

View File

@ -0,0 +1,13 @@
import * as plugins from './cloudly.plugins.js';
import * as paths from './cloudly.paths.js';
import { Cloudly } from './index.js';
export class CloudlyInfo {
public cloudlyRef: Cloudly;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
}

View File

@ -0,0 +1,79 @@
import * as plugins from './cloudly.plugins.js';
import * as paths from './cloudly.paths.js';
import { logger } from './cloudly.logging.js';
import type { Cloudly } from './cloudly.classes.cloudly.js';
/**
* the main cloudly config
*/
export class CloudlyConfig {
public cloudlyRef: Cloudly;
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
public data: plugins.servezoneInterfaces.data.ICloudlyConfig
// authentication and settings
public smartjwtInstance: plugins.smartjwt.SmartJwt;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public async init() {
this.appData = await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>({
envMapping: {
cfToken: 'CF_TOKEN',
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
letsEncryptEmail: 'hard:domains@lossless.org',
hetznerToken: 'HETZNER_API_TOKEN',
letsEncryptPrivateKey: null,
publicUrl: 'SERVEZONE_URL',
publicPort: 'SERVEZONE_PORT',
mongoDescriptor: {
mongoDbUrl: 'MONGODB_URL',
mongoDbName: 'MONGODB_DATABASE',
mongoDbUser: 'MONGODB_USER',
mongoDbPass: 'MONGODB_PASSWORD',
},
s3Descriptor: {
endpoint: 'S3_ENDPOINT',
accessKey: 'S3_ACCESSKEY',
accessSecret: 'S3_SECRETKEY',
port: 'S3_PORT', // Note: This will remain as a string. Ensure to parse it to an integer where it's used.
useSsl: true,
},
sslMode: 'SERVEZONE_SSLMODE' as plugins.servezoneInterfaces.data.ICloudlyConfig['sslMode'],
},
requiredKeys: [
'cfToken',
'hetznerToken',
'letsEncryptEmail',
'publicUrl',
'publicPort',
'sslMode',
'environment',
'mongoDescriptor',
],
});
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
const kvStore = await this.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeys', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
this.data = await kvStore.readAll();
const missingKeys = await this.appData.logMissingKeys();
if (missingKeys.length > 0) {
logger.log('error', `missing keys: ${missingKeys.join(', ')}`);
throw new Error('missing keys');
}
}
}

View File

@ -0,0 +1,96 @@
import * as plugins from './cloudly.plugins.js';
import * as paths from './cloudly.paths.js';
import { Cloudly } from './cloudly.classes.cloudly.js';
import { logger } from './cloudly.logging.js';
/**
* handles incoming requests from CI to deploy new versions of apps
*/
export class CloudlyServer {
/**
* a reference to the cloudly instance
*/
private cloudlyRef: Cloudly;
/**
* the smartexpress server handling the actual requests
*/
public typedServer: plugins.typedserver.TypedServer;
public typedsocketServer: plugins.typedsocket.TypedSocket;
/**
* typedrouter
* @param cloudlyArg
*/
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(cloudlyArg: Cloudly) {
this.cloudlyRef = cloudlyArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
}
// =========
// LIFECYCLE
// =========
/**
* init the reception instance
*/
public async start() {
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`)
let sslCert: plugins.smartacme.Cert;
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`)
logger.log('info', `This might take 10 minutes...`)
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
this.cloudlyRef.config.data.publicUrl
);
logger.log('success', `Successfully obtained certificate for cloudly domain ${this.cloudlyRef.config.data.publicUrl}`)
} else if (this.cloudlyRef.config.data.sslMode === 'external') {
logger.log('info', `Using external certificate for ssl mode, meaning cloudly is not in charge of ssl termination.`)
}
interface IRequestGuardData {
req: plugins.typedserver.Request;
res: plugins.typedserver.Response;
}
// guards
const guardIp = new plugins.smartguard.Guard<IRequestGuardData>(async (dataArg) => {
if (true) {
return true;
} else {
dataArg.res.status(500);
dataArg.res.send(`Not allowed to perform this operation!`);
dataArg.res.end();
return false;
}
});
// server
this.typedServer = new plugins.typedserver.TypedServer({
cors: true,
forceSsl: false,
port: this.cloudlyRef.config.data.publicPort,
...(sslCert ? {
privateKey: sslCert.privateKey,
publicKey: sslCert.publicKey,
} : {}),
injectReload: true,
serveDir: paths.distServeDir,
watch: true,
enableCompression: true,
preferredCompressionMethod: 'gzip',
});
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
await this.typedServer.start();
}
/**
* stop the reception instance
*/
public async stop() {
await this.typedServer.stop();
}
}

16
ts/cloudly.logging.ts Normal file
View File

@ -0,0 +1,16 @@
import * as plugins from './cloudly.plugins.js';
import * as paths from './cloudly.paths.js';
export const logger = new plugins.smartlog.Smartlog({
logContext: {
company: null,
environment: null,
runtime: null,
zone: null,
companyunit: null,
containerName: null,
}
});
logger.enableConsole({
captureAll: false
});

5
ts/cloudly.paths.ts Normal file
View File

@ -0,0 +1,5 @@
import * as plugins from './cloudly.plugins.js';
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
export const nogitDir = plugins.path.join(packageDir, '.nogit/');
export const distServeDir = plugins.path.join(packageDir, './dist_serve');

77
ts/cloudly.plugins.ts Normal file
View File

@ -0,0 +1,77 @@
// node native
import * as path from 'path';
export { path };
// @apiglobal scope
import * as typedrequest from '@api.global/typedrequest';
import * as typedsocket from '@api.global/typedsocket';
export { typedrequest, typedsocket };
// @mojoio scope
import * as cloudflare from '@apiclient.xyz/cloudflare';
import * as digitalocean from '@apiclient.xyz/digitalocean';
import * as hetznercloud from '@apiclient.xyz/hetznercloud';
import * as slack from '@apiclient.xyz/slack';
export { cloudflare, digitalocean, hetznercloud, slack };
// @tsclass scope
import * as tsclass from '@tsclass/tsclass';
export { tsclass };
// @push.rocks scope
import * as npmextra from '@push.rocks/npmextra';
import * as projectinfo from '@push.rocks/projectinfo';
import * as qenv from '@push.rocks/qenv';
import * as smartacme from '@push.rocks/smartacme';
import * as smartbucket from '@push.rocks/smartbucket';
import * as smartcli from '@push.rocks/smartcli';
import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartexit from '@push.rocks/smartexit';
import * as typedserver from '@api.global/typedserver';
import * as smartfile from '@push.rocks/smartfile';
import * as smartguard from '@push.rocks/smartguard';
import * as smartjson from '@push.rocks/smartjson';
import * as smartjwt from '@push.rocks/smartjwt';
import * as smartlog from '@push.rocks/smartlog';
import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartssh from '@push.rocks/smartssh';
import * as smartstring from '@push.rocks/smartstring';
import * as smartunique from '@push.rocks/smartunique';
import * as taskbuffer from '@push.rocks/taskbuffer';
export {
npmextra,
projectinfo,
qenv,
smartacme,
smartbucket,
smartcli,
smartdata,
smartexit,
typedserver,
smartdelay,
smartfile,
smartguard,
smartjson,
smartjwt,
smartlog,
smartpath,
smartpromise,
smartrequest,
smartssh,
smartstring,
smartunique,
taskbuffer,
};
// @servezone scope
import * as servezoneInterfaces from '@serve.zone/interfaces';
export { servezoneInterfaces };

View File

@ -0,0 +1,19 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
/**
* the portion of Cloudflare responsible
*/
export class CloudflareConnector {
private cloudlyRef: Cloudly;
public cloudflare: plugins.cloudflare.CloudflareAccount;
constructor(cloudlyArg: Cloudly) {
this.cloudlyRef = cloudlyArg;
}
// init the instance
public async init() {
this.cloudflare = new plugins.cloudflare.CloudflareAccount(this.cloudlyRef.config.data.cfToken);
}
}

View File

@ -0,0 +1,46 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
export class LetsencryptConnector {
private cloudlyRef: Cloudly;
private smartacme: plugins.smartacme.SmartAcme;
constructor(cloudlyArg: Cloudly) {
this.cloudlyRef = cloudlyArg;
}
public async getCertificateForDomain(domainName: string) {
const cert = await this.smartacme.getCertificateForDomain(domainName);
return cert;
}
/**
* inits letsencrypt
*/
public async init() {
this.smartacme = new plugins.smartacme.SmartAcme({
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail,
accountPrivateKey: this.cloudlyRef.config.data.letsEncryptPrivateKey,
environment: this.cloudlyRef.config.data.environment,
setChallenge: async (dnsChallenge) => {
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeSetDnsChallenge(
dnsChallenge
);
},
removeChallenge: async (dnsChallenge) => {
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeRemoveDnsChallenge(
dnsChallenge
);
},
mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor,
});
await this.smartacme.init();
}
/**
* stops the instance
*/
public async stop() {
await this.smartacme.stop();
}
}

View File

@ -0,0 +1,21 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
export class MongodbConnector {
// INSTANCE
private cloudlyRef: Cloudly;
public smartdataDb: plugins.smartdata.SmartdataDb;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public async init() {
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.cloudlyRef.config.data.mongoDescriptor);
await this.smartdataDb.init();
}
public async stop() {
await this.smartdataDb.close();
}
}

View File

View File

@ -0,0 +1,83 @@
import * as plugins from '../cloudly.plugins.js';
// Create an array to hold 10 ISecretGroup objects
const demoSecretGroups: plugins.servezoneInterfaces.data.ISecretGroup[] = [];
// Generate 10 ISecretGroup objects
for (let i = 1; i <= 10; i++) {
const secretGroup: plugins.servezoneInterfaces.data.ISecretGroup = {
id: `${plugins.smartunique.shortId(8)}`,
data: {
name: `Demo Secret Group ${i}`,
description: `This is a demo secret group for testing purposes ${i}`,
key: `CI_RUNNER_TOKEN_${i}`,
priority: i,
tags: [
{
key: 'project',
value: `my_project_${i}`,
},
{
key: 'environment',
value: i % 2 === 0 ? 'staging' : 'production',
},
],
environments: {
production: {
value: `prod_secret_value_${i}`,
lastUpdated: 1630522000 + i,
history: [
{
timestamp: String(1630521000 + i),
value: `old_prod_value_${i}`,
},
],
},
staging: {
value: `stag_secret_value_${i}`,
updateToken: `updateToken${i}`,
lastUpdated: 1630522500 + i,
history: [
{
timestamp: String(1630521500 + i),
value: `old_stag_value_${i}`,
},
],
},
},
},
};
// Push each ISecretGroup object into the array
demoSecretGroups.push(secretGroup);
}
// Create an array to hold 10 IConfigBundle objects
const demoConfigBundles: plugins.servezoneInterfaces.data.ISecretBundle[] = [];
// Generate 10 IConfigBundle objects that match demoSecretGroups
for (let i = 0; i < demoSecretGroups.length; i++) {
const secretGroup = demoSecretGroups[i];
const configBundle: plugins.servezoneInterfaces.data.ISecretBundle = {
id: `configBundleId${i + 1}`,
data: {
name: `Demo Config Bundle ${i + 1}`,
description: 'Demo Purpose',
includedSecretGroupIds: [secretGroup.id],
includedTags: secretGroup.data.tags,
authorizations: Object.keys(secretGroup.data.environments).map((env) => {
return {
secretAccessKey: `mockSecretAccessKeyFor${env}`,
environment: env,
};
}),
},
};
// Push each IConfigBundle object into the array
demoConfigBundles.push(configBundle);
}
// Exporting the array of demo IConfigBundle objects
export { demoSecretGroups, demoConfigBundles };

38
ts/demo/index.ts Normal file
View File

@ -0,0 +1,38 @@
import type { Cloudly } from '../cloudly.classes.cloudly.js';
export const installDemoData = async (cloudlyRef: Cloudly) => {
// ================================================================================
// SECRETS
const demoDataSecrets = await import('./demo.data.secrets.js');
const secretGroups = await cloudlyRef.secretManager.CSecretGroup.getInstances({});
for (const secretGroup of secretGroups) {
await secretGroup.delete();
}
const secretBundles = await cloudlyRef.secretManager.CSecretBundle.getInstances({});
for (const secretBundle of secretBundles) {
await secretBundle.delete();
}
for (const secretData of demoDataSecrets.demoSecretGroups) {
const secretGroup = new cloudlyRef.secretManager.CSecretGroup();
Object.assign(secretGroup, secretData);
await secretGroup.save();
}
for (const secretBundleData of demoDataSecrets.demoConfigBundles) {
const secretBundle = new cloudlyRef.secretManager.CSecretBundle();
Object.assign(secretBundle, secretBundleData);
await secretBundle.save();
}
// ================================================================================
// CLUSTERS
const clusters = await cloudlyRef.clusterManager.CCluster.getInstances({});
for (const cluster of clusters) {
await cluster.delete();
}
}

28
ts/index.ts Normal file
View File

@ -0,0 +1,28 @@
import * as early from '@push.rocks/early';
early.start('cloudly');
import * as plugins from './cloudly.plugins.js';
import * as paths from './cloudly.paths.js';
import { Cloudly } from './cloudly.classes.cloudly.js';
import { logger } from './cloudly.logging.js';
const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true);
early.stop();
/**
* starts the cloudly instance
*/
const runCli = async () => {
logger.log('info', process.env.SERVEZONE_ENVIRONMENT);
const cloudlyInstance = new Cloudly();
logger.log(
'info',
`running in environment ${await cloudlyQenv.getEnvVarOnDemand('SERVEZONE_ENVIRONMENT')}`
);
await cloudlyInstance.start();
const demoMod = await import('./demo/index.js');
demoMod.installDemoData(cloudlyInstance);
};
export { runCli, Cloudly };

View File

@ -0,0 +1,31 @@
import * as plugins from '../cloudly.plugins.js';
/*
* cluster defines a swarmkit cluster
*/
@plugins.smartdata.Manager()
export class Cluster extends plugins.smartdata.SmartDataDbDoc<Cluster, plugins.servezoneInterfaces.data.ICluster> {
// STATIC
public static async fromConfigObject(
configObjectArg: plugins.servezoneInterfaces.data.ICluster
) {
const newCluster = new Cluster();
Object.assign(newCluster, configObjectArg);
return newCluster;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ICluster['data'];
constructor() {
super();
}
public async getServices(): Promise<plugins.servezoneInterfaces.data.IService[]> {
return [];
}
}

View File

@ -0,0 +1,131 @@
import * as plugins from '../cloudly.plugins.js';
import * as paths from '../cloudly.paths.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
import { logger } from '../cloudly.logging.js';
import { Cluster } from './cluster.js';
export class ClusterManager {
public ready = plugins.smartpromise.defer();
public cloudlyRef: Cloudly;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public typedrouter = new plugins.typedrequest.TypedRouter();
public CCluster = plugins.smartdata.setDefaultManagerForDoc(this, Cluster);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
const cluster = await this.storeCluster({
id: plugins.smartunique.uniSimple('cluster'),
data: {
name: dataArg.clusterName,
jumpCode: plugins.smartunique.uniSimple('cluster'),
jumpCodeUsedAt: null,
secretKey: plugins.smartunique.shortId(16),
acmeInfo: null,
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
servers: [],
sshKeys: [],
},
});
console.log(await cluster.createSavableObject());
this.cloudlyRef.serverManager.ensureServerInfrastructure();
return {
clusterConfig: await cluster.createSavableObject(),
};
})
);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_GetAllClusters>(
new plugins.typedrequest.TypedHandler('getAllClusters', async (dataArg) => {
// TODO: do authentication here
const clusters = await this.getAllClusters();
return {
clusters: await Promise.all(
clusters.map((clusterArg) => clusterArg.createSavableObject())
),
};
})
);
}
public async init() {
// lets read the config folder
logger.log('info', 'config manager is now initializing');
this.ready.resolve();
}
/**
* gets a cluster config by Name
*/
public async getClusterConfigBy_ServerIP() {
await this.ready.promise;
// TODO: implement getclusterConfigByServerIp
}
public async getClusterConfigBy_JumpCode(jumpCodeArg: string) {
await this.ready.promise;
return await Cluster.getInstance({
data: {
jumpCode: jumpCodeArg,
},
});
}
public async getClusterConfigBy_ClusterIdentifier(
clusterIdentifier: plugins.servezoneInterfaces.data.IClusterIdentifier
) {
await this.ready.promise;
return await Cluster.getInstance({
data: {
name: clusterIdentifier.clusterName,
secretKey: clusterIdentifier.secretKey,
},
});
}
/**
* get config by id
*/
public async getConfigBy_ConfigID(configId: string) {
await this.ready.promise;
return await Cluster.getInstance({
id: configId,
});
}
/**
* gets all cluster configs
*/
public async getAllClusters() {
await this.ready.promise;
return await Cluster.getInstances({});
}
/**
* allows storage of a config
* @param configName
* @param configObjectArg
*/
public async storeCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
let clusterInstance = await Cluster.getInstance({ id: configObjectArg.id });
if (!clusterInstance) {
clusterInstance = await Cluster.fromConfigObject(configObjectArg);
} else {
Object.assign(clusterInstance, configObjectArg);
}
await clusterInstance.save();
return clusterInstance;
}
}

View File

@ -0,0 +1,73 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
/**
* in charge of talking to coreflow services on clusters
* coreflow runs on a server when ServerManager is done.
*/
export class CloudlyCoreflowManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>(
new plugins.typedrequest.TypedHandler('getIdentityByJumpCode', async (requestData) => {
const clusterConfig =
await this.cloudlyRef.clusterManager.getClusterConfigBy_JumpCode(
requestData.jumpCode
);
if (!clusterConfig) {
throw new plugins.typedrequest.TypedResponseError('The supplied jumpCode is not valid.');
}
return {
clusterIdentifier: {
clusterName: clusterConfig.data.name,
secretKey: clusterConfig.data.secretKey,
},
};
})
);
// lets enable the getting of cluster configs
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig>(
'getClusterConfig',
async (dataArg) => {
const clusterIdentifier = dataArg.clusterIdentifier;
console.log('trying to get clusterConfigSet');
console.log(dataArg);
const clusterConfigSet =
await this.cloudlyRef.clusterManager.getClusterConfigBy_ClusterIdentifier(
clusterIdentifier
);
console.log('got cluster config and sending it back to coreflow');
return {
configData: await clusterConfigSet.createSavableObject()
};
}
)
);
// lets enable getting of certificates
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetSslCertificate>(
'getSslCertificate',
async (dataArg) => {
console.log(`got request for certificate ${dataArg.requiredCertName}`);
const cert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
dataArg.requiredCertName
);
console.log(`got certificate ready for reponse ${dataArg.requiredCertName}`);
return {
certificate: await cert.createSavableObject(),
};
}
)
);
}
}

View File

@ -0,0 +1,21 @@
import * as plugins from '../cloudly.plugins.js';
import type { ImageManager } from './classes.imagemanager.js';
@plugins.smartdata.Manager()
export class Image extends plugins.smartdata.SmartDataDbDoc<Image, plugins.servezoneInterfaces.data.IImage, ImageManager> {
public static async create(imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>) {
const image = new Image();
image.id = plugins.smartunique.uni('image');
Object.assign(image.data, imageDataArg);
await image.save();
return image;
}
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IImage['data'];
public async getVersions() {}
}

View File

@ -0,0 +1,82 @@
import type { Cloudly } from '../cloudly.classes.cloudly.js';
import * as plugins from '../cloudly.plugins.js';
import { Image } from './classes.image.js';
export class ImageManager {
cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public typedrouter = new plugins.typedrequest.TypedRouter();
public CImage = plugins.smartdata.setDefaultManagerForDoc(this, Image);
smartbucketInstance: plugins.smartbucket.SmartBucket;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
'getAllImages',
async (requestArg) => {
const images = await this.CImage.getInstances({});
return {
images: await Promise.all(
images.map((image) => {
return image.createSavableObject();
})
),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
'createImage',
async (reqArg) => {
const image = await this.CImage.create({
name: reqArg.name,
});
return {
image: await image.createSavableObject(),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DownloadImage>(
'pullImage',
async (reqArg) => {
const image = await this.CImage.getInstance({
data: {
name: reqArg.name,
}
});
const imageVersion =
}
)
);
}
public async start() {
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
console.log(this.cloudlyRef.config.data.s3Descriptor);
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
this.cloudlyRef.config.data.s3Descriptor
);
const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test');
await bucket.fastStore('test/test.txt', 'hello');
}
public async createImage(nameArg: string) {
const newImage = await this.CImage.create({
name: nameArg,
});
}
}

View File

@ -0,0 +1,15 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
/**
* takes care of receiving and providing logs
*/
export class LogManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
}
}

View File

@ -0,0 +1,62 @@
// a secret bundle is a set of secrets ready to be used in a project.
// it bundles secretgroups
import { SecretGroup } from './classes.secretgroup.js';
import * as plugins from '../cloudly.plugins.js';
@plugins.smartdata.Manager()
export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
SecretBundle,
plugins.servezoneInterfaces.data.ISecretBundle
> {
// STATIC
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ISecretBundle['data'];
public async getSecretGroups() {
const secretGroups: SecretGroup[] = [];
for (const secretGroupId of this.data.includedSecretGroupIds) {
secretGroups.push(
await SecretGroup.getInstance({
id: secretGroupId,
})
);
}
return secretGroups;
}
/**
* searches the secretGroups for environments and returns them
*/
public async getEnvironments() {
const environments = new Set();
const secretGroups = await this.getSecretGroups();
for (const secretGroup of secretGroups) {
environments.add(secretGroup.data.environments);
}
return Array.from(environments);
}
public async getAuthorizationFromAuthKey(authKeyArg: string) {
const authorization = this.data.authorizations.find((authArg) => {
return authArg.secretAccessKey === authKeyArg;
});
return authorization;
}
public async getKeyValueObjectForEnvironment(environmentArg: string) {
const secretGroups = await this.getSecretGroups();
const returnObject = {};
for (const secretGroup of secretGroups) {
if (!secretGroup.data.environments[environmentArg]) {
continue;
}
returnObject[secretGroup.data.key] = secretGroup.data.environments[environmentArg].value;
}
return returnObject;
}
}

View File

@ -0,0 +1,21 @@
/**
* a secretgroup is a set of secrets for different environments.
*/
import * as plugins from '../cloudly.plugins.js';
@plugins.smartdata.Manager()
export class SecretGroup extends plugins.smartdata.SmartDataDbDoc<
SecretGroup,
plugins.servezoneInterfaces.data.ISecretGroup
> {
// INSTANCE
/**
* the insatnce id. This should be a random id, except for default
*/
@plugins.smartdata.unI()
id: string;
@plugins.smartdata.svDb()
data: plugins.servezoneInterfaces.data.ISecretGroup['data'];
}

View File

@ -0,0 +1,156 @@
import * as plugins from '../cloudly.plugins.js';
import * as paths from '../cloudly.paths.js';
import { SecretBundle } from './classes.secretbundle.js';
import { SecretGroup } from './classes.secretgroup.js';
import { logger } from '../cloudly.logging.js';
import type { Cloudly } from '../cloudly.classes.cloudly.js';
/**
* The `ConfigVault` class provides methods for reading and writing configuration data to a file.
* It uses the `TypedServer` and `TypedRouter` classes from the `configvault.plugins.js` module to handle HTTP requests and route them to the appropriate handlers.
*
* @class
*/
export class CloudlySecretManager {
// attached classes
public CSecretBundle = plugins.smartdata.setDefaultManagerForDoc(this, SecretBundle);
public CSecretGroup = plugins.smartdata.setDefaultManagerForDoc(this, SecretGroup);
// INSTANCE
public cloudlyRef: Cloudly;
public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
public serviceQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
public typedrouter: plugins.typedrequest.TypedRouter;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public async start() {
// lets set up a typedrouter
this.typedrouter = new plugins.typedrequest.TypedRouter();
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
// console.log(dataArg);
if (dataArg.username !== 'admin' || dataArg.password !== 'password') {
logger.log('warn', 'login failed');
} else {
jwt = await this.cloudlyRef.config.smartjwtInstance.createJWT({
status: 'loggedIn',
});
logger.log('success', 'login successful');
}
return {
jwt,
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(
'adminGetConfigBundlesAndSecretGroups',
async (dataArg) => {
dataArg.jwt
const secretBundles = await SecretBundle.getInstances({});
const secretGroups = await SecretGroup.getInstances({});
return {
secretBundles: [
...(await Promise.all(
secretBundles.map((configBundle) => configBundle.createSavableObject())
)),
],
secretGroups: [
...(await Promise.all(
secretGroups.map((secretGroup) => secretGroup.createSavableObject())
)),
],
};
}
)
);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_CreateConfigBundlesAndSecretGroups>(
new plugins.typedrequest.TypedHandler(
'adminCreateConfigBundlesAndSecretGroups',
async (dataArg) => {
for (const secretGroupObject of dataArg.secretGroups) {
const secretGroup = new SecretGroup();
secretGroup.id = plugins.smartunique.shortId(8);
secretGroup.data = secretGroupObject.data;
await secretGroup.save();
}
return {
ok: true,
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_DeleteConfigBundlesAndSecretGroups>(
'adminDeleteConfigBundlesAndSecretGroups',
async (dataArg) => {
for (const secretGroupId of dataArg.secretGroupIds) {
const secretGroup = await SecretGroup.getInstance({
id: secretGroupId,
});
await secretGroup.delete();
}
for (const secretBundleId of dataArg.secretBundleIds) {
const configBundle = await SecretBundle.getInstance({
id: secretBundleId,
});
await configBundle.delete();
console.log(`deleted configbundle ${secretBundleId}`);
}
return {
ok: true,
};
}
)
);
// lets add typedrouter routes for accessing the configvailt from apps
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_GetEnvBundle>(
'getEnvBundle',
async (dataArg) => {
const wantedBundle = await SecretBundle.getInstance({
data: {
authorizations: {
// @ts-ignore
$elemMatch: {
secretAccessKey: dataArg.authorization,
},
},
},
});
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
dataArg.authorization
);
return {
envBundle: {
configKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
authorization.environment
),
environment: authorization.environment,
timeSensitive: false,
},
};
}
)
);
}
public async stop() {}
}

View File

@ -0,0 +1,39 @@
import * as plugins from '../cloudly.plugins.js';
/*
* cluster defines a swarmkit cluster
*/
@plugins.smartdata.Manager()
export class Server extends plugins.smartdata.SmartDataDbDoc<Server, plugins.servezoneInterfaces.data.IServer> {
// STATIC
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer
) {
const newServer = new Server();
newServer.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IServer['data'] = {
assignedClusterId: hetznerServerArg.data.labels.clusterId,
requiredDebianPackages: [],
sshKeys: [],
type: 'hetzner',
}
Object.assign(newServer, { data });
await newServer.save();
return newServer;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IServer['data'];
constructor() {
super();
}
public async getServices(): Promise<plugins.servezoneInterfaces.data.IService[]> {
return [];
}
}

View File

@ -0,0 +1,103 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
import { Cluster } from '../manager.cluster/cluster.js';
import { Server } from './server.js';
export class CloudlyServerManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CServer = plugins.smartdata.setDefaultManagerForDoc(this, Server);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
/**
* is used be serverconfig module on the server to get the actual server config
*/
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
'getServerConfig',
async (requestData) => {
const serverId = requestData.serverId;
const server = await this.CServer.getInstance({
id: serverId,
})
return {
configData: await server.createSavableObject(),
};
}
)
);
}
public async start() {
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(this.cloudlyRef.config.data.hetznerToken);
}
public async stop() {}
/**
* creates the server infrastructure on hetzner
* ensures that there are exactly the reources that are needed
* no more, no less
*/
public async ensureServerInfrastructure() {
// get all clusters
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
for (const cluster of allClusters) {
// get existing servers
const servers = await this.getServersByCluster(cluster);
// if there is no server, create one
if (servers.length === 0) {
const server = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('server'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
}
});
const newServer = await Server.createFromHetznerServer(server);
console.log(`cluster created new server for cluster ${cluster.id}`);
} else {
console.log(`cluster ${cluster.id} already has servers. Making sure that they actually exist in the real world...`);
// if there is a server, make sure that it exists
for (const server of servers) {
const hetznerServer = await this.hetznerAccount.getServersByLabel({
'clusterId': cluster.id
});
if (!hetznerServer) {
console.log(`server ${server.id} does not exist in the real world. Creating it now...`);
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('server'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
}
});
const newServer = await Server.createFromHetznerServer(hetznerServer);
}
}
}
}
}
public async getServersByCluster(clusterArg: Cluster) {
const results = await this.CServer.getInstances({
data: {
assignedClusterId: clusterArg.id,
}
});
return results;
}
}

View File

View File

@ -0,0 +1,22 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../index.js';
/**
* external api manager manages external api requests
*/
export class ExternalApiManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.network.IRequest_Any_Cloudly_GetNetworkNodes>(
new plugins.typedrequest.TypedHandler('getNetworkNodes', async (requestData) => {
const networkNodes = [];
return {
networkNodes,
};
})
);
}
}

View File

@ -0,0 +1,35 @@
import * as plugins from '../cloudly.plugins.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
import { logger } from '../cloudly.logging.js';
export class CloudlyTaskmanager {
public cloudlyRef: Cloudly;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public everyMinuteTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
public everyHourTask = new plugins.taskbuffer.Task({
taskFunction: async () => {
logger.log('info', `Performing hourly maintenance check.`);
const configs = await this.cloudlyRef.clusterManager.getAllClusters();
logger.log('info', `Got ${configs.length} configs`);
configs.map((configArg) => {
console.log(configArg.name);
});
},
});
public everyDayTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
public everyWeekTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
}

View File

@ -0,0 +1,32 @@
import * as plugins from '../cloudly.plugins.js';
/*
A container version is managed by the versionmanager
*/
@plugins.smartdata.Manager()
export class ContainerVersion
extends plugins.smartdata.SmartDataDbDoc<ContainerVersion, unknown>
implements plugins.servezoneInterfaces.data.IContainerVersionData
{
public static async fromIVersionData(
dataArg: plugins.servezoneInterfaces.data.IContainerVersionData
) {
const containerVersionInstance = new ContainerVersion();
containerVersionInstance.id = plugins.smartunique.shortId();
Object.assign(containerVersionInstance, dataArg);
return containerVersionInstance;
}
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public dockerImageUrl: string;
@plugins.smartdata.svDb()
public dockerImageVersion: string;
constructor() {
super();
}
}

View File

@ -0,0 +1,149 @@
import * as plugins from '../cloudly.plugins.js';
import { ContainerVersion } from './containerversion.js';
import { Cloudly } from '../cloudly.classes.cloudly.js';
export class CloudlyVersionManager {
// INSTANCE
public cloudlyRef: Cloudly;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public typedRouter = new plugins.typedrequest.TypedRouter();
// connected classes
public CContainerVersion = plugins.smartdata.setDefaultManagerForDoc(this, ContainerVersion);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
// get version
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.version.IRequest_Any_Cloudly_VersionManager_GetLatestContainerVersion>(
new plugins.typedrequest.TypedHandler(
'getLatestContainerVersion',
async (typedRequestData) => {
const containerVersionGet: ContainerVersion =
await ContainerVersion.getInstance<ContainerVersion>({
dockerImageUrl: typedRequestData.dockerImageUrl,
});
return {
dockerImageUrl: containerVersionGet.dockerImageUrl,
dockerImageVersion: containerVersionGet.dockerImageVersion,
};
}
)
);
// update version
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.version.IRequest_Any_Cloudly_VersionManager_InformCloudlyAboutNewContainerVersion>(
new plugins.typedrequest.TypedHandler(
'informCloudlyAboutNewContainerVersion',
async (dataArg) => {
console.log(`Got a container version announcement! "${dataArg.dockerImageUrl}"`);
let containerVersion: ContainerVersion =
await ContainerVersion.getInstance<ContainerVersion>({
dockerImageUrl: dataArg.dockerImageUrl,
});
if (containerVersion) {
containerVersion.dockerImageVersion = dataArg.dockerImageVersion;
await containerVersion.save();
} else {
containerVersion = await ContainerVersion.fromIVersionData(dataArg);
await containerVersion.save();
}
// lets push this info to the relevant clusters
const clusters = await this.cloudlyRef.clusterManager.getAllClusters();
let foundServices: plugins.servezoneInterfaces.data.IService;
let relevantClusterIdentifier: plugins.servezoneInterfaces.data.IClusterIdentifier;
for (const clusterArg of clusters) {
console.log(clusterArg);
for (const serviceArg of await clusterArg.getServices()) {
if (serviceArg.image === containerVersion.dockerImageUrl) {
foundServices = serviceArg;
break;
}
}
if (foundServices) {
relevantClusterIdentifier = {
clusterName: clusterArg.data.name,
secretKey: clusterArg.data.secretKey,
};
break;
}
}
if (!relevantClusterIdentifier) {
console.log('no cluster found that needs to update');
return {};
} else {
console.log('found relevant cluster identifier:');
console.log(relevantClusterIdentifier);
}
const targetConnection =
await this.cloudlyRef.server.typedsocketServer.findTargetConnection(
async (connectionArg) => {
const identityTag = await connectionArg.getTagById('identity');
if (!identityTag) {
return false;
}
const result =
plugins.smartjson.stringify(identityTag.payload) ===
plugins.smartjson.stringify(relevantClusterIdentifier);
return result;
}
);
if (targetConnection) {
console.log(`the relevant cluster is connected and is now being informed.`);
const informCoreflowTR =
this.cloudlyRef.server.typedsocketServer.createTypedRequest<plugins.servezoneInterfaces.requests.version.IRequest_Cloudly_Coreflow_VersionManager_InformCoreflowAboutNewContainerVersion>(
'informCoreflowAboutNewContainerVersion',
targetConnection
);
informCoreflowTR.fire({
dockerImageUrl: containerVersion.dockerImageUrl,
dockerImageVersion: containerVersion.dockerImageVersion,
});
} else {
console.log('the relevant cluster is not connected at this time.');
}
return {};
}
)
);
// lets support the servezone standard
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.IRequest_InformAboutNewContainerImage>(
'servezonestandard_InformAboutNewContainerVersion',
async (dataArg) => {
const result =
await this.typedRouter.routeAndAddResponse<plugins.servezoneInterfaces.requests.version.IRequest_Any_Cloudly_VersionManager_InformCloudlyAboutNewContainerVersion>(
{
method: 'informCloudlyAboutNewContainerVersion',
request: {
dockerImageUrl: dataArg.containerImageInfo.registryUrl,
dockerImageVersion: dataArg.containerImageInfo.version,
},
response: null
},
true
);
return result.response;
}
)
);
}
/**
* gets all versions
*/
public async getAllVersions() {
const result = await ContainerVersion.getInstances<ContainerVersion>({});
return result;
}
}

View File

@ -0,0 +1,13 @@
import * as plugins from './plugins.js';
export class CloudlyClient {
public clientToken: string;
constructor(clientToken: string) {
this.clientToken = clientToken;
}
public async getClusters() {
}
}

View File

@ -0,0 +1,5 @@
import * as plugins from './plugins.js';
export class Cluster {
public getServers() {}
}

View File

@ -0,0 +1,5 @@
import * as plugins from './plugins.js';
export class Image {
public getImages() {}
}

View File

View File

@ -0,0 +1,5 @@
import * as plugins from './plugins.js';
export class Service {
}

1
ts_apiclient/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './classes.cloudlyclient.js';

5
ts_apiclient/plugins.ts Normal file
View File

@ -0,0 +1,5 @@
import * as typedrequest from '@api.global/typedrequest';
export {
typedrequest
}

0
ts_cliclient/index.ts Normal file
View File

View File

@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
*/
export const commitinfo = {
name: '@serve.zone/cloudly',
version: '1.0.214',
description: 'A cloud manager utilizing Docker Swarmkit, designed for operations on Cloudron, and supports various cloud platforms like DigitalOcean, Hetzner Cloud, and Cloudflare.'
}

192
ts_web/appstate.ts Normal file
View File

@ -0,0 +1,192 @@
import * as plugins from './plugins.js';
import * as domtools from '@design.estate/dees-domtools';
const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate();
export interface ILoginState {
jwt: string;
}
export const loginStatePart = await appstate.getStatePart<ILoginState>(
'login',
{ jwt: null },
'persistent'
);
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
async (statePartArg, payloadArg) => {
const currentState = statePartArg.getState();
const trLogin =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'/typedrequest',
'adminLoginWithUsernameAndPassword'
);
const response = await trLogin.fire({
username: payloadArg.username,
password: payloadArg.password,
});
return {
...currentState,
...(response.jwt ? { jwt: response.jwt } : {}),
};
}
);
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
const currentState = statePartArg.getState();
return {
...currentState,
jwt: null,
};
});
export interface IDataState {
secretGroups?: plugins.interfaces.data.ISecretGroup[];
secretBundles?: plugins.interfaces.data.ISecretBundle[];
clusters?: plugins.interfaces.data.ICluster[];
images?: any[];
services?: any[];
deployments?: any[];
dns?: any[];
mails?: any[];
logs?: any[];
s3?: any[];
dbs?: any[];
backups?: any[];
}
export const dataState = await appstate.getStatePart<IDataState>(
'data',
{
secretGroups: [],
secretBundles: [],
clusters: [],
images: [],
services: [],
deployments: [],
dns: [],
mails: [],
logs: [],
s3: [],
dbs: [],
backups: [],
},
'soft'
);
// Getting data
export const getDataAction = dataState.createAction(async (statePartArg) => {
let currentState = statePartArg.getState();
// Secrets
const trGetSecrets =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(
'/typedrequest',
'adminGetConfigBundlesAndSecretGroups'
);
const response = await trGetSecrets.fire({
jwt: loginStatePart.getState().jwt,
});
currentState = {
...currentState,
...response,
};
// Clusters
const trGetClusters =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_GetAllClusters>(
'/typedrequest',
'getAllClusters'
);
const responseClusters = await trGetClusters.fire({
jwt: loginStatePart.getState().jwt,
});
currentState = {
...currentState,
...responseClusters,
}
return currentState;
});
// SecretGroup Actions
export const createSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
let currentState = statePartArg.getState();
const trCreateSecretGroup =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secret.IReq_Admin_CreateConfigBundlesAndSecretGroups>(
'/typedrequest',
'adminCreateConfigBundlesAndSecretGroups'
);
const response = await trCreateSecretGroup.fire({
jwt: loginStatePart.getState().jwt,
secretBundles: [],
secretGroups: [payloadArg],
});
currentState = await dataState.dispatchAction(getDataAction, null);
return currentState;
return currentState;
}
);
export const deleteSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: { secretGroupId: string }) => {
let currentState = statePartArg.getState();
const trDeleteSecretGroup =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secret.IReq_Admin_DeleteConfigBundlesAndSecretGroups>(
'/typedrequest',
'adminDeleteConfigBundlesAndSecretGroups'
);
const response = await trDeleteSecretGroup.fire({
jwt: loginStatePart.getState().jwt,
secretBundleIds: [],
secretGroupIds: [payloadArg.secretGroupId],
});
currentState = await dataState.dispatchAction(getDataAction, null);
return currentState;
}
);
// SecretBundle Actions
export const deleteSecretBundleAction = dataState.createAction(
async (statePartArg, payloadArg: { configBundleId: string }) => {
let currentState = statePartArg.getState();
const trDeleteConfigBundle =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secret.IReq_Admin_DeleteConfigBundlesAndSecretGroups>(
'/typedrequest',
'adminDeleteConfigBundlesAndSecretGroups'
);
const response = await trDeleteConfigBundle.fire({
jwt: loginStatePart.getState().jwt,
secretBundleIds: [payloadArg.configBundleId],
secretGroupIds: [],
});
currentState = await dataState.dispatchAction(getDataAction, null);
return currentState;
}
);
// cluster
export const addClusterAction = dataState.createAction(
async (
statePartArg,
payloadArg: {
clusterName: string;
}
) => {
let currentState = statePartArg.getState();
const trAddCluster =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_CreateCluster>(
'/typedrequest',
'createCluster'
);
const response = await trAddCluster.fire({
jwt: loginStatePart.getState().jwt,
...payloadArg,
});
currentState = {
...currentState,
...{
clusters: [...currentState.clusters, response.clusterConfig],
},
}
return currentState;
}
);

View File

@ -0,0 +1,202 @@
import { commitinfo } from '../00_commitinfo_data.js';
import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import {
DeesElement,
css,
cssManager,
customElement,
html,
state
} from '@design.estate/dees-element';
import { CloudlyViewBackups } from './cloudly-view-backups.js';
import { CloudlyViewClusters } from './cloudly-view-clusters.js';
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
import { CloudlyViewDns } from './cloudly-view-dns.js';
import { CloudlyViewImages } from './cloudly-view-images.js';
import { CloudlyViewLogs } from './cloudly-view-logs.js';
import { CloudlyViewMails } from './cloudly-view-mails.js';
import { CloudlyViewOverview } from './cloudly-view-overview.js';
import { CloudlyViewS3 } from './cloudly-view-s3.js';
import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js';
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js';
import { CloudlyViewServices } from './cloudly-view-services.js';
declare global {
interface HTMLElementTagNameMap {
'cvault-dashboard': CloudlyDashboard;
}
}
@customElement('cloudly-dashboard')
export class CloudlyDashboard extends DeesElement {
@state() private jwt: string;
@state() private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
clusters: [],
};
constructor() {
super();
const subcription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subcription);
}
public static styles = [
cssManager.defaultStyles,
css`
.maincontainer {
position: relative;
width: 100vw;
height: 100vh;
}
h1 {
font-weight: 400;
font-size: 24px;
font-family: 'Cal Sans';
}
`,
];
public render() {
return html`
<div class="maincontainer">
<dees-simple-login name="cloudly v${commitinfo.version}">
<dees-simple-appdash name="cloudly v${commitinfo.version}"
.viewTabs=${[
{
name: 'Overview',
element: CloudlyViewOverview,
},
{
name: 'SecretGroups',
element: CloudlyViewSecretGroups,
},
{
name: 'SecretBundles',
element: CloudlyViewSecretBundles,
},
{
name: 'Clusters',
element: CloudlyViewClusters,
},
{
name: 'Images',
element: CloudlyViewImages,
},
{
name: 'Services',
element: CloudlyViewServices,
},
{
name: 'Deployments',
element: CloudlyViewDeployments,
},
{
name: 'DNS',
element: CloudlyViewDns,
},
{
name: 'Mails',
element: CloudlyViewMails,
},
{
name: 'Logs',
element: CloudlyViewLogs,
},
{
name: 's3',
element: CloudlyViewS3,
},
{
name: 'DBs',
element: CloudlyViewDbs,
},
{
name: 'Backups',
element: CloudlyViewBackups,
},
{
name: 'Fleet',
element: CloudlyViewBackups,
}
] as plugins.deesCatalog.IView[]}
></dees-simple-appdash>
</dees-simple-login>
</div>
`;
}
public async firstUpdated() {
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
simpleLogin.addEventListener('login', (e: CustomEvent) => {
console.log(e.detail);
this.login(e.detail.data.username, e.detail.data.password);
});
this.addEventListener('contextmenu', (eventArg) => {
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'About',
iconName: 'mugHot',
action: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'About',
content: html`configvault ${commitinfo.version}`,
menuOptions: [
{
name: 'close',
iconName: null,
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
},
},
]);
});
// lets deal with initial state
const domtools = await this.domtoolsPromise;
const loginState = appstate.loginStatePart.getState();
console.log(loginState);
if (loginState.jwt) {
this.jwt = loginState.jwt;
await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getDataAction, null);
}
}
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
console.log(`attempting to login...`);
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
const form = simpleLogin.shadowRoot.querySelector('dees-form');
form.setStatus('pending', 'Logging in...');
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
username,
password,
});
if (state.jwt) {
console.log('got jwt');
this.jwt = state.jwt;
form.setStatus('success', 'Logged in!');
await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getDataAction, null);
} else {
form.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000);
form.reset();
}
}
private async logout() {}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-backups')
export class CloudlyViewBackups extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Backups</cloudly-sectionheading>
<dees-table
.heading1=${'Backups'}
.heading2=${'decoded in client'}
.data=${this.data.backups}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,140 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-clusters')
export class CloudlyViewClusters extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
<dees-table
.heading1=${'Clusters'}
.heading2=${'decoded in client'}
.data=${this.data.clusters}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
console.log(itemArg);
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add cluster',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Cluster',
content: html`
<dees-form>
<dees-input-text
.key=${'clusterName'}
.label=${'cluster name'}
.description=${'a descriptive name for the cluster'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'create',
action: async (modalArg) => {
const data: {
clusterName: string;
} = (await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData()) as any;
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
await modalArg.destroy();
},
},
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-dbs')
export class CloudlyViewDbs extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>DBs</cloudly-sectionheading>
<dees-table
.heading1=${'DBs'}
.heading2=${'decoded in client'}
.data=${this.data.dbs}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-deployments')
export class CloudlyViewDeployments extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
<dees-table
.heading1=${'Deployments'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-dns')
export class CloudlyViewDns extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>DNS</cloudly-sectionheading>
<dees-table
.heading1=${'DNS'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,367 @@
import * as plugins from '../plugins.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-images')
export class CloudlyViewImages extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Images</cloudly-sectionheading>
<dees-table
heading1="SecretGroups"
heading2="decoded in client"
.data=${this.data.images}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
return {
name: secretGroup.data.name,
priority: secretGroup.data.priority,
tags: html`<dees-chips
.selectionMode=${'none'}
.selectableChips=${secretGroup.data.tags}
></dees-chips>`,
key: secretGroup.data.key,
history: (() => {
const allHistory = [];
for (const environment in secretGroup.data.environments) {
allHistory.push(...secretGroup.data.environments[environment].history);
}
return allHistory.length;
})(),
};
}}
.dataActions=${[
{
name: 'add SecretGroup',
type: ['header', 'footer'],
iconName: 'plus',
actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new SecretGroup',
content: html`
<dees-form>
<dees-input-text
.label=${'name'}
.key=${'data.name'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'description'}
.key=${'data.description'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'Secret Key (data.key)'}
.key=${'data.key'}
.value=${''}
></dees-input-text>
<dees-table
.heading1=${'Environments'}
.heading2=${'keys need to be unique'}
key="environments"
.data=${[
{
environment: 'production',
value: '',
},
{
environment: 'staging',
value: '',
},
]}
.dataActions=${[
{
name: 'add environment',
iconName: 'plus',
type: ['footer'],
actionFunc: async (dataArg) => {
dataArg.table.data.push({
environment: 'new environment',
value: '',
});
dataArg.table.requestUpdate('data');
},
},
{
name: 'delete environment',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (dataArg) => {
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
dataArg.table.requestUpdate('data');
},
},
] as plugins.deesCatalog.ITableAction[]}
.editableFields=${['environment', 'value']}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'save',
action: async (modalArg) => {
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
const formData = await deesForm.collectFormData();
console.log(`Prepare saving of data:`);
console.log(formData);
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of formData['environments'] as any[]) {
environments[itemArg.environment] = {
value: itemArg.value,
history: [],
lastUpdated: Date.now(),
};
}
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
id: null,
data: {
name: formData['data.name'] as string,
description: formData['data.description'] as string,
key: formData['data.key'] as string,
environments,
tags: [],
},
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
type: ['contextmenu', 'inRow', 'doubleClick'],
iconName: 'penToSquare',
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const environmentsArray: Array<
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
environment: string;
}
> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
environmentsArray.push({
environment: environmentName,
...dataArg.item.data.environments[environmentName],
});
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit Secret',
content: html`
<dees-form>
<dees-input-text
.key=${'id'}
.disabled=${true}
.label=${'ID'}
.value=${dataArg.item.id}
></dees-input-text>
<dees-input-text
.key=${'data.name'}
.disabled=${false}
.label=${'name'}
.value=${dataArg.item.data.name}
></dees-input-text>
<dees-input-text
.key=${'data.description'}
.disabled=${false}
.label=${'description'}
.value=${dataArg.item.data.description}
></dees-input-text>
<dees-input-text
.key=${'data.key'}
.disabled=${false}
.label=${'key'}
.value=${dataArg.item.data.key}
></dees-input-text>
<dees-table
.key=${'environments'}
.heading1=${'Environments'}
.heading2=${'double-click to edit values'}
.data=${environmentsArray.map((itemArg) => {
return {
environment: itemArg.environment,
value: itemArg.value,
};
})}
.editableFields=${['environment', 'value']}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (actionDataArg) => {
actionDataArg.table.data.splice(
actionDataArg.table.data.indexOf(actionDataArg.item),
1
);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: null,
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Save',
iconName: null,
action: async (modalArg) => {
const data = await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData();
console.log(data);
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
id: dataArg.item.id,
data: {
name: data['data.name'] as string,
description: data['data.description'] as string,
key: data['data.key'] as string,
environments: {},
tags: dataArg.item.data.tags,
},
};
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of data['environments'] as any[]) {
}
},
},
],
});
},
},
{
name: 'history',
iconName: 'clockRotateLeft',
type: ['contextmenu', 'inRow'],
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const historyArray: Array<{
environment: string;
value: string;
}> = [];
for (const environment of Object.keys(dataArg.item.data.environments)) {
for (const historyItem of dataArg.item.data.environments[environment].history) {
historyArray.push({
environment,
value: historyItem.value,
});
}
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `history for ${dataArg.item.data.key}`,
content: html`
<dees-table
.data=${historyArray}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
) => {
console.log('delete', itemArg);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`,
menuOptions: [
{
name: 'close',
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ${itemArg.item.data.key}`,
content: html`
<div style="text-align:center">Do you really want to delete the secret?</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${itemArg.item.data.key}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
console.log(`Delete ${itemArg.item.id}`);
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, {
secretGroupId: itemArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-logs')
export class CloudlyViewLogs extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Logs</cloudly-sectionheading>
<dees-table
.heading1=${'Logs'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-mails')
export class CloudlyViewMails extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Mails</cloudly-sectionheading>
<dees-table
.heading1=${'Mails'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,71 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-overview')
export class CloudlyViewOverview extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 8px 16px;
}
.clusterGrid {
display: grid;
grid-template-columns: ${cssManager.cssGridColumns(3, 8)};
grid-gap: 16px;
margin-bottom: 40px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Overview</cloudly-sectionheading>
${this.data.clusters.length === 0 ? html`
You need to create at least one cluster to see an overview.
`: html``}
${this.data.clusters.map(
(clusterArg) => html`
<dees-label .label=${'cluster: ' + clusterArg.data.name}></dees-label>
<div class="clusterGrid">
<dees-chart-area .label=${'System Usage'}></dees-chart-area>
<dees-chart-area .label=${'Internet Traffic'}></dees-chart-area>
<dees-chart-area .label=${'Requests'}></dees-chart-area>
<dees-chart-area .label=${'WebSocket Connections'}></dees-chart-area>
<dees-chart-log class="services" .label=${'Deployed Services'}></dees-chart-log>
<dees-chart-log class="eventLog" .label=${'Event Log'}></dees-chart-log>
</div>
`
)}
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-s3')
export class CloudlyViewS3 extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>S3</cloudly-sectionheading>
<dees-table
.heading1=${'S3'}
.heading2=${'decoded in client'}
.data=${this.data.s3}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,173 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretbundles')
export class CloudlyViewSecretBundles extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
<dees-table
.heading1=${'SecretBundles'}
.heading2=${'decoded in client'}
.data=${this.data.secretBundles}
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
return {
name: itemArg.data.name,
secretGroups: (() => {
const secretGroupIds = itemArg.data.includedSecretGroupIds;
let secretGroupNames: string[] = [];
for (const secretGroupId of secretGroupIds) {
const secretGroup = this.data.secretGroups.find(
(secretGroupArg) => secretGroupArg.id === secretGroupId
);
if (secretGroup) {
secretGroupNames.push(secretGroup.data.name);
}
}
return secretGroupNames.join(', ');
})(),
tags: html`<dees-chips
.selectionMode=${'none'}
.selectableChips=${itemArg.data.includedTags}
></dees-chips>`,
};
}}
.dataActions=${[
{
name: 'add SecretBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add SecretBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
iconName: 'edit',
type: ['doubleClick', 'contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit SecretBundle',
content: html`
<dees-form>
<dees-input-text .label=${'purpose'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'save', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,367 @@
import * as plugins from '../plugins.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretsgroups')
export class CloudlyViewSecretGroups extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
<dees-table
heading1="SecretGroups"
heading2="decoded in client"
.data=${this.data.secretGroups}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
return {
name: secretGroup.data.name,
priority: secretGroup.data.priority,
tags: html`<dees-chips
.selectionMode=${'none'}
.selectableChips=${secretGroup.data.tags}
></dees-chips>`,
key: secretGroup.data.key,
history: (() => {
const allHistory = [];
for (const environment in secretGroup.data.environments) {
allHistory.push(...secretGroup.data.environments[environment].history);
}
return allHistory.length;
})(),
};
}}
.dataActions=${[
{
name: 'add SecretGroup',
type: ['header', 'footer'],
iconName: 'plus',
actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new SecretGroup',
content: html`
<dees-form>
<dees-input-text
.label=${'name'}
.key=${'data.name'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'description'}
.key=${'data.description'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'Secret Key (data.key)'}
.key=${'data.key'}
.value=${''}
></dees-input-text>
<dees-table
.heading1=${'Environments'}
.heading2=${'keys need to be unique'}
key="environments"
.data=${[
{
environment: 'production',
value: '',
},
{
environment: 'staging',
value: '',
},
]}
.dataActions=${[
{
name: 'add environment',
iconName: 'plus',
type: ['footer'],
actionFunc: async (dataArg) => {
dataArg.table.data.push({
environment: 'new environment',
value: '',
});
dataArg.table.requestUpdate('data');
},
},
{
name: 'delete environment',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (dataArg) => {
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
dataArg.table.requestUpdate('data');
},
},
] as plugins.deesCatalog.ITableAction[]}
.editableFields=${['environment', 'value']}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'save',
action: async (modalArg) => {
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
const formData = await deesForm.collectFormData();
console.log(`Prepare saving of data:`);
console.log(formData);
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of formData['environments'] as any[]) {
environments[itemArg.environment] = {
value: itemArg.value,
history: [],
lastUpdated: Date.now(),
};
}
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
id: null,
data: {
name: formData['data.name'] as string,
description: formData['data.description'] as string,
key: formData['data.key'] as string,
environments,
tags: [],
},
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
type: ['contextmenu', 'inRow', 'doubleClick'],
iconName: 'penToSquare',
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const environmentsArray: Array<
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
environment: string;
}
> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
environmentsArray.push({
environment: environmentName,
...dataArg.item.data.environments[environmentName],
});
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit Secret',
content: html`
<dees-form>
<dees-input-text
.key=${'id'}
.disabled=${true}
.label=${'ID'}
.value=${dataArg.item.id}
></dees-input-text>
<dees-input-text
.key=${'data.name'}
.disabled=${false}
.label=${'name'}
.value=${dataArg.item.data.name}
></dees-input-text>
<dees-input-text
.key=${'data.description'}
.disabled=${false}
.label=${'description'}
.value=${dataArg.item.data.description}
></dees-input-text>
<dees-input-text
.key=${'data.key'}
.disabled=${false}
.label=${'key'}
.value=${dataArg.item.data.key}
></dees-input-text>
<dees-table
.key=${'environments'}
.heading1=${'Environments'}
.heading2=${'double-click to edit values'}
.data=${environmentsArray.map((itemArg) => {
return {
environment: itemArg.environment,
value: itemArg.value,
};
})}
.editableFields=${['environment', 'value']}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (actionDataArg) => {
actionDataArg.table.data.splice(
actionDataArg.table.data.indexOf(actionDataArg.item),
1
);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: null,
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Save',
iconName: null,
action: async (modalArg) => {
const data = await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData();
console.log(data);
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
id: dataArg.item.id,
data: {
name: data['data.name'] as string,
description: data['data.description'] as string,
key: data['data.key'] as string,
environments: {},
tags: dataArg.item.data.tags,
},
};
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of data['environments'] as any[]) {
}
},
},
],
});
},
},
{
name: 'history',
iconName: 'clockRotateLeft',
type: ['contextmenu', 'inRow'],
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const historyArray: Array<{
environment: string;
value: string;
}> = [];
for (const environment of Object.keys(dataArg.item.data.environments)) {
for (const historyItem of dataArg.item.data.environments[environment].history) {
historyArray.push({
environment,
value: historyItem.value,
});
}
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `history for ${dataArg.item.data.key}`,
content: html`
<dees-table
.data=${historyArray}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
) => {
console.log('delete', itemArg);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`,
menuOptions: [
{
name: 'close',
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ${itemArg.item.data.key}`,
content: html`
<div style="text-align:center">Do you really want to delete the secret?</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${itemArg.item.data.key}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
console.log(`Delete ${itemArg.item.id}`);
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, {
secretGroupId: itemArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@ -0,0 +1,133 @@
import * as plugins from '../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-services')
export class CloudlyViewServices extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Services</cloudly-sectionheading>
<dees-table
.heading1=${'Services'}
.heading2=${'decoded in client'}
.data=${this.data.services}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

4
ts_web/elements/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './shared/index.js';
export * from './cloudly-dashboard.js';
export * from './cloudly-view-secretgroups.js';
export * from './cloudly-view-secretbundles.js';

View File

@ -0,0 +1,33 @@
import * as plugins from '../../plugins.js';
import {
customElement,
html,
DeesElement,
property,
type TemplateResult,
cssManager,
css,
unsafeCSS,
type CSSResult,
state,
} from '@design.estate/dees-element';
@customElement('cloudly-sectionheading')
export class CloudlySectionheading extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
h1 {
font-family: 'Cal Sans';
letter-spacing: 0.025em;
}
`,
]
public render() {
return html`
<h1><slot></slot></h1>
`;
}
}

View File

@ -0,0 +1 @@
export * from './cloudly-sectionheading.js';

9
ts_web/index.ts Normal file
View File

@ -0,0 +1,9 @@
import * as plugins from './plugins.js';
import { html } from '@design.estate/dees-element';
import './elements/index.js';
plugins.deesElement.render(html`
<cloudly-dashboard></cloudly-dashboard>
`, document.body);

19
ts_web/plugins.ts Normal file
View File

@ -0,0 +1,19 @@
import * as interfaces from '@serve.zone/interfaces';
export {
interfaces
}
// @design.estate scope
import * as deesDomtools from '@design.estate/dees-domtools';
import * as deesElement from '@design.estate/dees-element';
import * as deesCatalog from '@design.estate/dees-catalog';
export { deesDomtools, deesElement, deesCatalog };
// @push.rocks scope
import * as webjwt from '@push.rocks/webjwt';
export {
webjwt,
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"exclude": [
"dist_*/**/*.d.ts"
]
}