Compare commits
144 Commits
Author | SHA1 | Date | |
---|---|---|---|
1580bb1585 | |||
af7fcf6c2e | |||
23c9e3f678 | |||
7d4e766e9e | |||
907f3e8320 | |||
bc7a2ca5f1 | |||
77d911e47a | |||
b9c9c2d0a9 | |||
d5b91789d1 | |||
eb8350f453 | |||
b987ce27b8 | |||
630e363e53 | |||
a602021952 | |||
80585437a0 | |||
4674a20a2c | |||
820cdfcd48 | |||
6e5dd9b05a | |||
f3d5c21fab | |||
04b278ee28 | |||
7084d76c43 | |||
41d7550e89 | |||
4bf361d3a6 | |||
d70617a90c | |||
62ad1655d5 | |||
caf3a095f2 | |||
89e44b2e5f | |||
a617f51b19 | |||
355e04fd1d | |||
89bd767bea | |||
e567ebbf21 | |||
33311348e2 | |||
d6e914edab | |||
da7b866f23 | |||
7654d780b1 | |||
dbd9b661c6 | |||
e19d0b4deb | |||
f0ebb719f7 | |||
c8e0666bc6 | |||
0d0b106f90 | |||
c9073df7cd | |||
f65200703d | |||
57970b3d10 | |||
b4d9f40c41 | |||
a219725ff6 | |||
4b993fc6b3 | |||
d453da709f | |||
50fac41c95 | |||
affce1fcd1 | |||
df67ebd27a | |||
ef5bfd435a | |||
db07db930c | |||
f6309f600c | |||
7477704905 | |||
db89d86242 | |||
b74ce05845 | |||
79db68a4a2 | |||
5a3ddcf39b | |||
fe6bfc0a83 | |||
36a481ecd1 | |||
f7b2e203ed | |||
27c98c4e32 | |||
79257908d0 | |||
b5ca898eac | |||
53ade28931 | |||
fff4c7642d | |||
dafe6574cc | |||
b70dad4996 | |||
17b0b50fbd | |||
91a0272ab3 | |||
efd22d4087 | |||
c9e32540bf | |||
8344f96983 | |||
3b77089d79 | |||
b6bce76043 | |||
cab57ab303 | |||
804f1f3b12 | |||
f0144fdd5b | |||
81f286cb2f | |||
1f12cb9f94 | |||
26490e8ddd | |||
38d2120c35 | |||
f80b8decbc | |||
28cd6d1b49 | |||
899e5b0a7d | |||
0eff7c7510 | |||
7789348f4e | |||
66a23a515b | |||
7c1082f5a9 | |||
15ea5adec6 | |||
da0dddcceb | |||
b5433e412f | |||
7eb6bf794c | |||
b244518fcb | |||
95d0396abb | |||
a830299cc9 | |||
10fc1d7fba | |||
614ed78928 | |||
ea7f6a6477 | |||
2d746a9d1c | |||
a9fab24961 | |||
5548d5a72d | |||
6b52e05a86 | |||
87e273c30e | |||
0f3f4b8e3f | |||
5f16d8e494 | |||
5148bd1fff | |||
41c54a070c | |||
6956524c6e | |||
7a1d933559 | |||
343acd4997 | |||
337d111cf6 | |||
f49dce92cd | |||
c6abfe69b8 | |||
9d52c62335 | |||
971abd19c9 | |||
084a11d7ed | |||
8f49f0cb4f | |||
320b3ed9eb | |||
7cd6bc0e33 | |||
a218805f27 | |||
6f56304e07 | |||
4eb62200e8 | |||
c105596455 | |||
5aa136b8d9 | |||
240516520a | |||
4e1f9464fe | |||
41f9f93d1c | |||
c502d410b1 | |||
53f96095c7 | |||
d212dfb9f9 | |||
0ec665516d | |||
acc642adf9 | |||
a6521708f7 | |||
206fe445bc | |||
a7ee92cde9 | |||
cdbab26008 | |||
1983c64b77 | |||
a6e3a7f5fe | |||
6dd687012f | |||
55b2872ffc | |||
2e6e7f6ca8 | |||
f453ce3126 | |||
b8dd84b8a6 | |||
338ed5ed75 |
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
|
@@ -6,13 +6,14 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
# NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
|
# NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
|
||||||
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
||||||
|
NPMCI_SECRET01: ${{ secrets.NPMCI_SECRET01 }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
@@ -27,7 +28,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +55,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@@ -74,7 +75,7 @@ jobs:
|
|||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-dbase:npmci
|
image: code.foss.global/host.today/ht-docker-dbase:npmci
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -82,15 +83,14 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
run: |
|
run: |
|
||||||
npmci docker login
|
npmci docker login
|
||||||
npmci docker build
|
npmci docker build
|
||||||
npmci docker test
|
npmci docker test
|
||||||
# npmci docker push gitea.lossless.digital
|
npmci docker push code.foss.global
|
||||||
npmci docker push dockerregistry.lossless.digital
|
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
needs: test
|
needs: test
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# gitzone dockerfile_service
|
# gitzone dockerfile_service
|
||||||
## STAGE 1 // BUILD
|
## STAGE 1 // BUILD
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1
|
FROM code.foss.global/host.today/ht-docker-node:npmci as node1
|
||||||
COPY ./ /app
|
COPY ./ /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NPMCI_TOKEN_NPM2
|
ARG NPMCI_TOKEN_NPM2
|
||||||
@@ -12,7 +12,7 @@ RUN pnpm run build
|
|||||||
|
|
||||||
# gitzone dockerfile_service
|
# gitzone dockerfile_service
|
||||||
## STAGE 2 // install production
|
## STAGE 2 // install production
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2
|
FROM code.foss.global/host.today/ht-docker-node:npmci as node2
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=node1 /app /app
|
COPY --from=node1 /app /app
|
||||||
RUN rm -rf .pnpm-store
|
RUN rm -rf .pnpm-store
|
||||||
@@ -24,7 +24,7 @@ RUN rm -rf node_modules/ && pnpm install --prod
|
|||||||
|
|
||||||
|
|
||||||
## STAGE 3 // rebuild dependencies for alpine
|
## STAGE 3 // rebuild dependencies for alpine
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3
|
FROM code.foss.global/host.today/ht-docker-node:alpine_npmci as node3
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=node2 /app /app
|
COPY --from=node2 /app /app
|
||||||
ARG NPMCI_TOKEN_NPM2
|
ARG NPMCI_TOKEN_NPM2
|
||||||
@@ -34,7 +34,7 @@ RUN pnpm config set store-dir .pnpm-store
|
|||||||
RUN pnpm rebuild -r
|
RUN pnpm rebuild -r
|
||||||
|
|
||||||
## STAGE 4 // the final production image with all dependencies in place
|
## STAGE 4 // the final production image with all dependencies in place
|
||||||
FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4
|
FROM code.foss.global/host.today/ht-docker-node:alpine as node4
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=node3 /app /app
|
COPY --from=node3 /app /app
|
||||||
|
|
||||||
|
@@ -0,0 +1 @@
|
|||||||
|
FROM serve.zone/cloudly:latest
|
||||||
|
456
changelog.md
Normal file
456
changelog.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-18 - 5.0.5 - fix(coreflow)
|
||||||
|
Fix Coreflow identity lookup and response shape; improve API client tests and bump dependencies
|
||||||
|
|
||||||
|
- ts/manager.coreflow/coreflowmanager.ts: Use $elemMatch to correctly query nested user.tokens when resolving identities and validate machine user types.
|
||||||
|
- ts/manager.coreflow/coreflowmanager.ts: Normalize getClusterConfig response to include services (was deploymentDirectives) and tidy handler signatures.
|
||||||
|
- test/test.apiclient.ts: Add detailed logging and improved error handling across preTask, client startup, identity retrieval, image creation and image upload to aid debugging and test observability.
|
||||||
|
- package.json: Update dependency versions (notable bumps): @types/node -> ^22.0.0, @push.rocks/smartacme -> ^8.0.0, @push.rocks/smartdata -> ^5.16.4, @push.rocks/smartexpect -> ^2.5.0, @push.rocks/smartpath -> ^6.0.0, @push.rocks/smartrequest -> ^4.2.2, plus other maintenance bumps.
|
||||||
|
- Add .claude/settings.local.json to provide local Claude permissions for developer tooling.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.4 - fix(platformservice/mta)
|
||||||
|
Update getEmailStatus response schema: make details property optional
|
||||||
|
|
||||||
|
- Changed details property from required with fixed message to optional with a flexible message structure in IReq_GetEMailStats response
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.3 - fix(mta)
|
||||||
|
update email status response type in MTA platform service
|
||||||
|
|
||||||
|
- Changed the response 'status' field in IRequest_CheckEmailStatus from a literal 'unknown' to a generic string for improved flexibility
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.2 - fix(platformservice/mta)
|
||||||
|
Refactor email status response in MTA service
|
||||||
|
|
||||||
|
- Updated IReq_CheckEmailStatus response: replaced union type ('ok' | 'not ok') with fixed status 'unknown' and added a details object with message 'Email not found'.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.1 - fix(mta)
|
||||||
|
Update email stats response interface in mta platform service to include totalEmailsSent, totalEmailsDelivered, totalEmailsBounced, averageDeliveryTimeMs, and lastUpdated timestamp.
|
||||||
|
|
||||||
|
- Modified IReq_GetEMailStats response in ts_interfaces/platformservice/mta.ts from an empty status object to a detailed email statistics structure.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.0 - BREAKING CHANGE(ts_interfaces/platformservice/mta)
|
||||||
|
Rename mta interfaces and upgrade dependency versions
|
||||||
|
|
||||||
|
- Upgraded devDependencies: @git.zone/tsbuild, tsbundle, tsdoc, tstest, tswatch, and @push.rocks/tapbundle to newer versions.
|
||||||
|
- Upgraded dependencies: @design.estate/dees-catalog, dees-domtools, dees-element, @push.rocks/smartdata, smartexpect, smartfile, smartpromise, smartrequest, smartrx, and tsclass (v4.2.0 to v9.0.0).
|
||||||
|
- Added new packageManager field in package.json and introduced pnpm-workspace.yaml for additional workspace configuration.
|
||||||
|
- Refactored mta API interfaces: renamed IRequest_SendEmail to IReq_SendEmail and IRequestRegisterRecipient to IReq_RegisterRecipient; added IReq_CheckEmailStatus and IReq_GetEMailStats.
|
||||||
|
|
||||||
|
## 2025-01-20 - 4.13.0 - feat(service)
|
||||||
|
Add support for service creation, update, and deletion.
|
||||||
|
|
||||||
|
- Implemented TypedHandlers for creating a new service.
|
||||||
|
- Added features to update existing service details.
|
||||||
|
- Enabled deletion of services by their unique ID.
|
||||||
|
|
||||||
|
## 2025-01-20 - 4.12.2 - fix(service)
|
||||||
|
Fix secret bundle and service management bugs
|
||||||
|
|
||||||
|
- Corrected the field name from 'includedImages' to 'imageClaims' in secret bundles.
|
||||||
|
- Implemented 'getFlatKeyValueObject' for secret bundles and modified related API interactions.
|
||||||
|
- Enhanced the Service class with methods for handling secret bundle data by resolving related groups and environments.
|
||||||
|
|
||||||
|
## 2025-01-02 - 4.12.1 - fix(deps)
|
||||||
|
Updated @git.zone/tspublish to version ^1.9.1
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-01-02 - 4.12.0 - feat(cli)
|
||||||
|
Add CLI support and external registries view
|
||||||
|
|
||||||
|
- Adds CLI client functionality
|
||||||
|
- Introduces a new view for External Registries in the dashboard
|
||||||
|
|
||||||
|
## 2024-12-30 - 4.11.0 - feat(external-registry)
|
||||||
|
Introduce external registry management
|
||||||
|
|
||||||
|
- Added ExternalRegistryManager to handle external registry operations.
|
||||||
|
- Implemented ability to create, retrieve, and delete external registries.
|
||||||
|
- Enhanced Cloudly class to include ExternalRegistryManager.
|
||||||
|
|
||||||
|
## 2024-12-29 - 4.10.0 - feat(apiclient)
|
||||||
|
Added support for managing external registries in the API client.
|
||||||
|
|
||||||
|
- Introduced methods to get a registry by ID, get all registries, and create a new registry in the externalRegistry object.
|
||||||
|
- Updated external registry request interfaces to match new API client capabilities.
|
||||||
|
|
||||||
|
## 2024-12-29 - 4.9.0 - feat(apiclient)
|
||||||
|
Add external registry management capabilities to Cloudly API client.
|
||||||
|
|
||||||
|
- Introduce ExternalRegistry class with methods for getting, creating, and updating external registries.
|
||||||
|
- Expand requests module to handle external registry management, including creation and deletion.
|
||||||
|
|
||||||
|
## 2024-12-28 - 4.8.1 - fix(interfaces)
|
||||||
|
Fix image location schema in IImage interface
|
||||||
|
|
||||||
|
- Refactored the 'external' object within IImage data to a 'location' object.
|
||||||
|
- Added 'internal' boolean to 'location' to specify internal/external status.
|
||||||
|
|
||||||
|
## 2024-12-28 - 4.8.0 - feat(manager.registry)
|
||||||
|
Add external registry management
|
||||||
|
|
||||||
|
- Introduced ExternalRegistry class for handling external registry configurations.
|
||||||
|
- Updated IExternalRegistry interface to include registry details.
|
||||||
|
- Enhanced IImage interface to support linking with external registries.
|
||||||
|
|
||||||
|
## 2024-12-28 - 4.7.1 - fix(secretmanagement)
|
||||||
|
Refactor secret bundle actions and improve authorization handling
|
||||||
|
|
||||||
|
- Refactored secret bundle handling by renaming methods and reorganizing static and instance methods in SecretBundle class.
|
||||||
|
- Added getSecretBundleByAuthorization method to SecretBundle.
|
||||||
|
- Improved getFlatKeyValueObjectForEnvironment to accurately retrieve key-value pairs for specified environments.
|
||||||
|
- Removed deprecated IEnvBundle interface and related request handler for better clarity and code usage.
|
||||||
|
- Updated request interfaces related to secret bundles for consistent method naming and arguments.
|
||||||
|
|
||||||
|
## 2024-12-22 - 4.7.0 - feat(apiclient)
|
||||||
|
Add method to flatten secret bundles into key-value objects.
|
||||||
|
|
||||||
|
- SecretBundle: Implemented toFlatKeyValueObject method to flatten secret groups into key-value pairs.
|
||||||
|
- Removed stale SecretManager class from apiclient.
|
||||||
|
|
||||||
|
## 2024-12-22 - 4.6.0 - feat(cloudlyapiclient)
|
||||||
|
Extend CloudlyApiClient with cluster, secretbundle, and secretgroup methods
|
||||||
|
|
||||||
|
- Added methods to CloudlyApiClient for managing clusters: getClusterById, getClusters, createCluster.
|
||||||
|
- Added methods to CloudlyApiClient for managing secret bundles: getSecretBundleById, getSecretBundles, createSecretBundle.
|
||||||
|
- Added methods to CloudlyApiClient for managing secret groups: getSecretGroupById, getSecretGroups, createSecretGroup.
|
||||||
|
|
||||||
|
## 2024-12-22 - 4.5.5 - fix(apiclient)
|
||||||
|
Fixed image creation method in cloudlyApiClient
|
||||||
|
|
||||||
|
- Corrected method call from 'images.createImage' to 'image.createImage' to ensure proper image creation.
|
||||||
|
- Updated cluster retrieval methods and ensured proper API routes are being called.
|
||||||
|
|
||||||
|
## 2024-12-21 - 4.5.4 - fix(ts_web)
|
||||||
|
Fix action type and data fields in appstate for CRUD operations
|
||||||
|
|
||||||
|
- Correct request method in createSecretGroupAction to accurately reflect the purpose.
|
||||||
|
- Align the deleteSecretGroupAction and deleteSecretBundleAction request types with proper interfaces.
|
||||||
|
- Ensure data payload matches backend requirements for secret group and secret bundle operations.
|
||||||
|
|
||||||
|
## 2024-12-21 - 4.5.3 - fix(secret-management)
|
||||||
|
Refactor secret management to use distinct secret bundle and group APIs. Introduce API client classes for secret bundles and groups.
|
||||||
|
|
||||||
|
- Updated secret management logic to separate secret bundle and group APIs.
|
||||||
|
- Implemented new API client classes for managing secret bundles and groups.
|
||||||
|
- Fixed incorrect method usages for secret-related actions.
|
||||||
|
|
||||||
|
## 2024-12-20 - 4.5.2 - fix(apiclient)
|
||||||
|
Implemented IService interface in Service class and improved secret bundle documentation.
|
||||||
|
|
||||||
|
- Implemented plugins.servezoneInterfaces.data.IService interface in the Service class within ts_apiclient.
|
||||||
|
- Updated documentation comments for the type property in the ISecretBundle interface.
|
||||||
|
|
||||||
|
## 2024-12-17 - 4.5.1 - fix(core)
|
||||||
|
Updated dependencies in package.json to latest versions.
|
||||||
|
|
||||||
|
- Bumped @git.zone/tswatch to version ^2.0.37
|
||||||
|
- Bumped @types/node to version ^22.10.2
|
||||||
|
- Bumped @design.estate/dees-catalog to version ^1.3.2
|
||||||
|
- Bumped @push.rocks/smartfile to version ^11.0.23
|
||||||
|
- Bumped @tsclass/tsclass to version ^4.2.0
|
||||||
|
|
||||||
|
## 2024-12-14 - 4.5.0 - feat(services)
|
||||||
|
Add service management functionalities
|
||||||
|
|
||||||
|
- Integrated service-related API client methods including getServices, getServiceById, and createService.
|
||||||
|
- Updated the deployment data structure in the service manager.
|
||||||
|
- Enhanced service interface to incorporate additional fields for comprehensive data handling.
|
||||||
|
- Ensured secure token generation for Cloudly authentication processes.
|
||||||
|
|
||||||
|
## 2024-11-18 - 4.4.0 - feat(api-client)
|
||||||
|
Add static method getImageById for Image class in api-client
|
||||||
|
|
||||||
|
- Introduced a static method getImageById in the Image class.
|
||||||
|
- Updated CloudlyApiClient to include the getImageById method in the images interface.
|
||||||
|
|
||||||
|
## 2024-11-18 - 4.3.21 - fix(interfaces)
|
||||||
|
Remove deprecated deployment directive and update related interfaces
|
||||||
|
|
||||||
|
- Removed IDeploymentDirective from data and requests.
|
||||||
|
- Updated IDeployment to remove references to directives.
|
||||||
|
- Changed IRequest_Any_Cloudly_GetClusterConfig to return services instead of deployment directives.
|
||||||
|
- Removed deploymentDirectiveIds from IService data structure.
|
||||||
|
|
||||||
|
## 2024-11-18 - 4.3.20 - fix(apiclient)
|
||||||
|
Ensure mandatory parameter in CloudlyApiClient constructor
|
||||||
|
|
||||||
|
- Made the 'optionsArg' parameter mandatory in the constructor of CloudlyApiClient class.
|
||||||
|
|
||||||
|
## 2024-11-18 - 4.3.19 - fix(docker)
|
||||||
|
Fix improper Docker push command preventing push to the correct registry.
|
||||||
|
|
||||||
|
- Corrected the docker push command in the '.gitea/workflows/docker_tags.yaml' file to push images to the 'code.foss.global' registry.
|
||||||
|
|
||||||
|
## 2024-11-17 - 4.3.18 - fix(docker_tags)
|
||||||
|
Updated Docker configuration to include NPM tokens.
|
||||||
|
|
||||||
|
- Restored NPMCI_TOKEN_NPM and NPMCI_TOKEN_NPM2 environment variables in docker_tags.yaml for authentication purposes.
|
||||||
|
|
||||||
|
## 2024-11-17 - 4.3.17 - fix(Dockerfile)
|
||||||
|
Corrected docker base image tag in Dockerfile for alpine compatibility.
|
||||||
|
|
||||||
|
- Updated Dockerfile to use the correct base image tag for Alpine.
|
||||||
|
- Resolved any potential build issues related to incorrect image tag usage.
|
||||||
|
|
||||||
|
## 2024-11-17 - 4.3.16 - fix(infrastructure)
|
||||||
|
Correct Docker image path in Dockerfile for improved build consistency.
|
||||||
|
|
||||||
|
- Dockerfile: Updated base image paths from 'code.foss.global/hosttoday/ht-docker-node' to 'code.foss.global/host.today/ht-docker-node'.
|
||||||
|
|
||||||
|
## 2024-11-17 - 4.3.15 - fix(project setup)
|
||||||
|
fixed incorrect configuration in npmextra.json
|
||||||
|
|
||||||
|
- Removed unnecessary 'dockerBuildargEnvMap' entry for NPMCI_TOKEN_NPM2.
|
||||||
|
- Corrected the githost and gitscope in gitzone module configuration.
|
||||||
|
- Updated the license field to reflect the correct license.
|
||||||
|
|
||||||
|
## 2024-11-16 - 4.3.14 - fix(docker tags)
|
||||||
|
Comment out unused secret variables in docker_tags.yaml
|
||||||
|
|
||||||
|
- Modified docker_tags.yaml to comment out unused secret variables related to NPM and GitHub tokens.
|
||||||
|
|
||||||
|
## 2024-11-16 - 4.3.13 - fix(package)
|
||||||
|
Updated package dependencies
|
||||||
|
|
||||||
|
- Updated @design.estate/dees-catalog version to 1.3.1
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.12 - fix(workflow)
|
||||||
|
Fix Docker image path in GitHub action workflow
|
||||||
|
|
||||||
|
- Corrected the path of the Docker image used in the GitHub action workflow from 'code.foss.global/hosttoday/ht-docker-dbase:npmci' to 'code.foss.global/host.today/ht-docker-dbase:npmci'.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.11 - fix(overall)
|
||||||
|
Refactor and improve code consistency across all modules
|
||||||
|
|
||||||
|
- Updated cloud configuration management for better reliability.
|
||||||
|
- Enhanced security measures in the authentication and authorization processes.
|
||||||
|
- Streamlined deployment logic in cluster management.
|
||||||
|
- Refactored code to improve maintainability and readability.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.10 - fix(dependencies)
|
||||||
|
Updated dependencies and fixed Docker Alpine image retrieval issue in tests
|
||||||
|
|
||||||
|
- Updated @push.rocks/tapbundle to version ^5.5.0 in devDependencies.
|
||||||
|
- Updated @push.rocks/smartrequest to version ^2.0.23 in dependencies.
|
||||||
|
- Ensured the Docker Alpine image is retrieved as a local tarball in cloudlyfactory.ts test helper.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.9 - fix(test and dependencies)
|
||||||
|
Corrected cloudlyUrl in test.apiclient and updated tapbundle dependency.
|
||||||
|
|
||||||
|
- Fixed cloudlyUrl assignment in the test.apiclient to use the correct helper method.
|
||||||
|
- Updated tapbundle dependency version from ^5.4.3 to ^5.4.4.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.8 - fix(api client)
|
||||||
|
Fixed localhost URL issue in test.client.ts
|
||||||
|
|
||||||
|
- Changed the cloudlyUrl in test.client.ts from 'localhost' to '127.0.0.1' to ensure consistency in network requests.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.7 - fix(tests)
|
||||||
|
Refactored test setup for consistency and isolated config initialization.
|
||||||
|
|
||||||
|
- test/helpers/cloudlyfactory.ts: Test configuration setup was refactored to ensure consistent initialization of cloudly configuration across tests.
|
||||||
|
- test/test.apiclient.ts: Updated cloudlyApiClient test setup to use testCloudlyConfig for dynamic port allocation.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.6 - fix(test)
|
||||||
|
Enhance test helpers with dynamic Hetzner token retrieval.
|
||||||
|
|
||||||
|
- Updated test/helpers/cloudlyfactory.ts to retrieve Hetzner token from environment variables.
|
||||||
|
- Expanded docker_tags workflow to securely handle and pass new environment secrets.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.5 - fix(helpers)
|
||||||
|
Add missing sslMode configuration to Cloudly config.
|
||||||
|
|
||||||
|
- Added the sslMode key with a value of 'none' to the Cloudly configuration object in test/helpers/cloudlyfactory.ts.
|
||||||
|
|
||||||
|
## 2024-11-06 - 4.3.4 - fix(testing)
|
||||||
|
Fixed Cloudly testing setup and dependencies
|
||||||
|
|
||||||
|
- Updated devDependency @push.rocks/tapbundle version from ^5.3.0 to ^5.4.3 in package.json.
|
||||||
|
- Updated devDependency @push.rocks/npmextra version from ^5.1.1 to ^5.1.2 in package.json.
|
||||||
|
- Improved the Cloudly test suite setup to ensure proper initialization of MongoDB and S3 services.
|
||||||
|
- Ensured asynchronous stopping and cleanup of MongoDB and S3 services post-test execution.
|
||||||
|
|
||||||
|
## 2024-11-05 - 4.3.3 - fix(core)
|
||||||
|
Fix configuration initialization by accepting a config argument
|
||||||
|
|
||||||
|
- Configuration initialization now accepts an optional config argument
|
||||||
|
- Updated test cloudly factory to use default public URL and port
|
||||||
|
- Updated dependencies versions
|
||||||
|
|
||||||
|
## 2024-11-05 - 4.3.2 - fix(npmextra)
|
||||||
|
Updated npm registry URL in npmextra.json
|
||||||
|
|
||||||
|
|
||||||
|
## 2024-11-05 - 4.3.1 - fix(package)
|
||||||
|
Update dependency version for @git.zone/tspublish
|
||||||
|
|
||||||
|
- Bump @git.zone/tspublish from version ^1.7.6 to ^1.7.7 in package.json
|
||||||
|
|
||||||
|
## 2024-11-05 - 4.3.0 - feat(dependencies)
|
||||||
|
Upgrade dependencies and include publish orders
|
||||||
|
|
||||||
|
- Upgraded @git.zone/tsbuild to version ^2.2.0
|
||||||
|
- Upgraded @git.zone/tspublish to version ^1.7.6
|
||||||
|
- Upgraded @types/node to version ^22.8.7
|
||||||
|
- Added publish order to ts_apiclient/tspublish.json and ts_interfaces/tspublish.json
|
||||||
|
|
||||||
|
## 2024-11-04 - 4.2.1 - fix(config)
|
||||||
|
Fix Docker image URL in Gitea workflow.
|
||||||
|
|
||||||
|
- Corrected the IMAGE URL from 'hosttoday' to 'host.today'.
|
||||||
|
|
||||||
|
## 2024-11-04 - 4.2.0 - feat(cloudron)
|
||||||
|
Add Dockerfile for Cloudron deployment
|
||||||
|
|
||||||
|
- Introduced a new Dockerfile for Cloudron deployment.
|
||||||
|
- The Dockerfile uses the latest version of cloudly as a base image.
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.1.3 - fix(dependency)
|
||||||
|
Updated dependency @git.zone/tspublish to version ^1.6.0
|
||||||
|
|
||||||
|
- Bumped @git.zone/tspublish version from ^1.5.5 to ^1.6.0 in package.json
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.1.2 - fix(core)
|
||||||
|
Corrected description and devDependencies
|
||||||
|
|
||||||
|
- Updated package.json description to accurately reflect features.
|
||||||
|
- Added `@git.zone/tsdoc` to devDependencies.
|
||||||
|
- Corrected version discrepancy in `@types/node` devDependency.
|
||||||
|
- Standardized description across multiple files including npmextra.json.
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.1.1 - fix(core)
|
||||||
|
Fixed syntax issues in commitinfo data and package.json file.
|
||||||
|
|
||||||
|
- Added a missing newline at the end of the package.json file.
|
||||||
|
- Corrected a trailing comma and added proper syntax in the commitinfo data.
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.1.0 - feat(core)
|
||||||
|
Enhance core functionality for cloud management and orchestration
|
||||||
|
|
||||||
|
- Improved initialization and management of cloud environments with Docker Swarmkit.
|
||||||
|
- Added capability to manage DNS records via Cloudflare.
|
||||||
|
- Introduced integration support for DigitalOcean resources.
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.0.1 - fix(package_manager)
|
||||||
|
Update @git.zone/tspublish dependency version
|
||||||
|
|
||||||
|
- Bump @git.zone/tspublish version from 1.5.4 to 1.5.5.
|
||||||
|
|
||||||
|
## 2024-10-28 - 4.0.0 - BREAKING CHANGE(core)
|
||||||
|
Significant overhaul with potential breaking changes, update to version 3.0.0
|
||||||
|
|
||||||
|
- Updated project version from 1.2.5 to 3.0.0 in package.json
|
||||||
|
|
||||||
|
## 2024-10-28 - 1.2.5 - fix(build)
|
||||||
|
Updated devDependencies for tspublish and removed buildDocs script
|
||||||
|
|
||||||
|
- devDependencies updated: @git.zone/tspublish to version ^1.5.4
|
||||||
|
- Removed: buildDocs script from the scripts section
|
||||||
|
|
||||||
|
## 2024-10-27 - 1.2.4 - fix(ci)
|
||||||
|
Fix Docker images and npm registry URL in CI workflows
|
||||||
|
|
||||||
|
- Updated Docker image registry URL from 'registry.gitlab.com' to 'code.foss.global'.
|
||||||
|
- Fixed npmci package installation path from '@shipzone/npmci' to '@ship.zone/npmci'.
|
||||||
|
|
||||||
|
## 2024-10-23 - 1.2.3 - fix(cli)
|
||||||
|
Set up CLI client definition and registry configuration
|
||||||
|
|
||||||
|
- Added a console log message for CLI identification.
|
||||||
|
- Defined package name for CLI client.
|
||||||
|
- Configured npm registries for package distribution.
|
||||||
|
|
||||||
|
## 2024-10-23 - 1.2.2 - fix(docs)
|
||||||
|
Updated documentation with clearer usage instructions and examples.
|
||||||
|
|
||||||
|
- Refined setup guides in README.
|
||||||
|
- Added more detailed examples in code snippets.
|
||||||
|
- Clarified cloud service integration instructions.
|
||||||
|
|
||||||
|
## 2024-10-23 - 1.2.1 - fix(core)
|
||||||
|
Fixed startup issue for the Cloudly instance
|
||||||
|
|
||||||
|
|
||||||
|
## 2024-10-21 - 1.2.0 - feat(cli)
|
||||||
|
Add tspublish.json for CLI client and interfaces
|
||||||
|
|
||||||
|
- Added registration details for publishing CLI client (@serve.zone/cli)
|
||||||
|
- Specified npm and custom registries in the tspublish.json for better publish control
|
||||||
|
- Updated dependency version for @git.zone/tspublish in package.json
|
||||||
|
|
||||||
|
## 2024-10-21 - 1.1.9 - fix(build)
|
||||||
|
Update Node types and other dependencies, add tspublish.json for api client
|
||||||
|
|
||||||
|
- Updated Node types devDependency to version ^22.7.7 from ^22.7.5.
|
||||||
|
- Updated smartbucket dependency to version ^3.0.23 from ^3.0.22.
|
||||||
|
- Added tspublish configuration file to the api client.
|
||||||
|
- Fixed the npm publish script in package.json.
|
||||||
|
|
||||||
|
## 2024-10-16 - 1.1.8 - fix(big fix upgrade)
|
||||||
|
fix: update dependency versions and address type errors
|
||||||
|
|
||||||
|
- Updated all listed dependencies in the package.json to their specified ranges.
|
||||||
|
- Fixed type mismatches and added missing imports in various TypeScript files.
|
||||||
|
- Refined existing tests and added a new helper to manage Docker image streams.
|
||||||
|
|
||||||
|
## 2024-08-25 - 1.1.7 - fix(deps)
|
||||||
|
Update dependencies to latest versions
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^2.1.80 to ^2.1.84
|
||||||
|
- Updated @push.rocks/tapbundle from ^5.0.23 to ^5.0.24
|
||||||
|
- Updated @types/node from ^20.14.6 to ^22.5.0
|
||||||
|
- Updated @apiclient.xyz/docker from ^1.2.2 to ^1.2.3
|
||||||
|
- Updated @design.estate/dees-catalog from ^1.0.289 to ^1.1.6
|
||||||
|
- Updated @design.estate/dees-element from ^2.0.34 to ^2.0.36
|
||||||
|
- Updated @git.zone/tsrun from ^1.2.37 to ^1.2.49
|
||||||
|
- Updated @push.rocks/smartbucket from ^3.0.20 to ^3.0.22
|
||||||
|
- Updated @push.rocks/smartpromise from ^4.0.3 to ^4.0.4
|
||||||
|
- Updated @serve.zone/interfaces from ^1.0.74 to ^1.0.78
|
||||||
|
- Updated @tsclass/tsclass from ^4.0.60 to ^4.1.2
|
||||||
|
|
||||||
|
## 2024-06-20 - 1.1.6 - Updates
|
||||||
|
Routine updates and fixes.
|
||||||
|
|
||||||
|
- (fix) core: update
|
||||||
|
|
||||||
|
## 2024-06-13 - 1.1.4 - Service Management Preparation
|
||||||
|
Incorporated updates and service management preparations.
|
||||||
|
|
||||||
|
- (fix) core: update
|
||||||
|
- (feat) prepare service management
|
||||||
|
|
||||||
|
## 2024-06-05 - 1.1.3 - CI Integration Improvement
|
||||||
|
Structural improvements and better CI integration preparation.
|
||||||
|
|
||||||
|
- (fix) structure: improve structure, prepare better CI integration
|
||||||
|
|
||||||
|
## 2024-06-02 - 1.1.2 - Image Manager Update
|
||||||
|
Prepared proper storage and retrieval of container images.
|
||||||
|
|
||||||
|
- (fix) imagemanager: prepare proper storage and retrieval of container images
|
||||||
|
|
||||||
|
## 2024-06-01 - 1.1.0 - Image Registry Work
|
||||||
|
Initiated work on image registry.
|
||||||
|
|
||||||
|
- (fix) image registry: start work on image registry
|
||||||
|
|
||||||
|
## 2024-05-30 - 1.0.216 - Enhanced Smartguards
|
||||||
|
Enhanced smartguards to verify action authorization.
|
||||||
|
|
||||||
|
- (feat) guards: use better smartguards to verify action authorization
|
||||||
|
|
||||||
|
## 2024-05-28 - 1.0.215 - Unified Package Update
|
||||||
|
Updated package unification for cloudly + API + CLI.
|
||||||
|
|
||||||
|
- (fix) switch to unified package for cloudly + API + CLI: update
|
||||||
|
|
||||||
|
## 2024-05-05 - 1.0.214 - Core Updates
|
||||||
|
Routine core updates.
|
||||||
|
|
||||||
|
- (fix) core: update
|
||||||
|
|
||||||
|
## 2024-04-20 - 1.0.213 - Core Update
|
||||||
|
Routine core updates.
|
||||||
|
|
||||||
|
- (fix) core: update
|
19
license
Normal file
19
license
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2014 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@@ -2,40 +2,48 @@
|
|||||||
"npmci": {
|
"npmci": {
|
||||||
"npmGlobalTools": [],
|
"npmGlobalTools": [],
|
||||||
"npmAccessLevel": "public",
|
"npmAccessLevel": "public",
|
||||||
"npmRegistryUrl": "verdaccio.lossless.one",
|
"npmRegistryUrl": "verdaccio.lossless.digital",
|
||||||
"dockerRegistryRepoMap": {
|
"dockerRegistryRepoMap": {
|
||||||
"registry.gitlab.com": "losslessone/services/servezone/cloudly"
|
"code.foss.global": "serve.zone/cloudly"
|
||||||
},
|
},
|
||||||
"dockerBuildargEnvMap": {
|
"dockerBuildargEnvMap": {}
|
||||||
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"gitzone": {
|
||||||
"projectType": "service",
|
"projectType": "service",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "servezone/private",
|
"gitscope": "serve.zone",
|
||||||
"gitrepo": "cloudly",
|
"gitrepo": "cloudly",
|
||||||
"description": "A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.",
|
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
||||||
"npmPackagename": "@serve.zone/cloudly",
|
"npmPackagename": "@serve.zone/cloudly",
|
||||||
"license": "UNLICENSED",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cloud management",
|
"multi-cloud management",
|
||||||
"Docker Swarmkit",
|
"Docker Swarmkit",
|
||||||
"multi-cloud",
|
"container orchestration",
|
||||||
|
"cloud services",
|
||||||
|
"API integration",
|
||||||
|
"web interface",
|
||||||
|
"CLI",
|
||||||
|
"CI/CD integration",
|
||||||
|
"cloud providers",
|
||||||
"DigitalOcean",
|
"DigitalOcean",
|
||||||
"Hetzner Cloud",
|
"Hetzner Cloud",
|
||||||
"Cloudflare",
|
"Cloudflare",
|
||||||
"container orchestration",
|
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"node.js",
|
"Node.js",
|
||||||
"infrastructure automation",
|
"infrastructure automation",
|
||||||
"Cloudron",
|
|
||||||
"configuration management",
|
|
||||||
"SSL management",
|
|
||||||
"APIs",
|
|
||||||
"devOps",
|
"devOps",
|
||||||
"cloud integration"
|
"secret management",
|
||||||
|
"configuration management",
|
||||||
|
"task scheduling",
|
||||||
|
"logging",
|
||||||
|
"SSL management",
|
||||||
|
"system logging",
|
||||||
|
"cloud API client",
|
||||||
|
"frontend",
|
||||||
|
"backend",
|
||||||
|
"security"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
108
package.json
108
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cloudly",
|
"name": "@serve.zone/cloudly",
|
||||||
"version": "1.1.0",
|
"version": "5.0.5",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.",
|
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
@@ -13,60 +13,67 @@
|
|||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||||
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
|
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
|
||||||
"start": "node cli.js",
|
"start": "node cli.js",
|
||||||
"startTs": "node cli.ts.js",
|
"startTs": "node cli.ts.js",
|
||||||
"watch": "tswatch website",
|
"watch": "tswatch website",
|
||||||
"localPublish": "gitzone commit"
|
"publish": "tspublish",
|
||||||
|
"docs": "tsdoc aidoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.80",
|
"@git.zone/tsbuild": "^2.6.7",
|
||||||
"@git.zone/tsbundle": "^2.0.15",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tsdoc": "^1.5.1",
|
||||||
"@git.zone/tswatch": "^2.0.23",
|
"@git.zone/tspublish": "^1.10.3",
|
||||||
"@push.rocks/tapbundle": "^5.0.23",
|
"@git.zone/tstest": "^2.3.5",
|
||||||
"@types/node": "^20.12.13"
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "3.0.28",
|
"@api.global/typedrequest": "3.1.10",
|
||||||
"@api.global/typedserver": "^3.0.50",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
|
"@api.global/typedserver": "^3.0.77",
|
||||||
"@api.global/typedsocket": "^3.0.1",
|
"@api.global/typedsocket": "^3.0.1",
|
||||||
"@apiclient.xyz/cloudflare": "^6.0.1",
|
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||||
"@apiclient.xyz/digitalocean": "^1.0.5",
|
"@apiclient.xyz/docker": "^1.3.0",
|
||||||
"@apiclient.xyz/hetznercloud": "^1.0.18",
|
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
||||||
"@apiclient.xyz/slack": "^3.0.9",
|
"@apiclient.xyz/slack": "^3.0.9",
|
||||||
"@design.estate/dees-catalog": "^1.0.289",
|
"@design.estate/dees-catalog": "^1.10.10",
|
||||||
"@design.estate/dees-domtools": "^2.0.57",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.0.34",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@git.zone/tsrun": "^1.2.37",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@push.rocks/early": "^4.0.3",
|
"@push.rocks/early": "^4.0.3",
|
||||||
"@push.rocks/npmextra": "^5.0.13",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/qenv": "^6.0.5",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^4.0.8",
|
"@push.rocks/smartacme": "^8.0.0",
|
||||||
"@push.rocks/smartbucket": "^3.0.9",
|
"@push.rocks/smartbucket": "^3.3.10",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartdata": "^5.2.1",
|
"@push.rocks/smartclickhouse": "^2.0.17",
|
||||||
|
"@push.rocks/smartdata": "^5.16.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartexit": "^1.0.23",
|
"@push.rocks/smartexit": "^1.0.23",
|
||||||
"@push.rocks/smartfile": "^11.0.15",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartguard": "^3.0.2",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjson": "^5.0.19",
|
"@push.rocks/smartjson": "^5.0.19",
|
||||||
"@push.rocks/smartjwt": "^2.0.4",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.0.6",
|
"@push.rocks/smartlog": "^3.1.8",
|
||||||
"@push.rocks/smartlog-destination-clickhouse": "^1.0.11",
|
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartlog-interfaces": "^3.0.2",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartrequest": "^2.0.22",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrx": "^3.0.7",
|
"@push.rocks/smartrequest": "^4.2.2",
|
||||||
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartssh": "^2.0.1",
|
"@push.rocks/smartssh": "^2.0.1",
|
||||||
|
"@push.rocks/smartstate": "^2.0.26",
|
||||||
|
"@push.rocks/smartstream": "^3.2.5",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "^3.0.2",
|
"@push.rocks/taskbuffer": "^3.0.2",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.9",
|
||||||
"@serve.zone/interfaces": "^1.0.56",
|
"@tsclass/tsclass": "^9.2.0"
|
||||||
"@tsclass/tsclass": "^4.0.54"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -92,21 +99,32 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/servezone/private/cloudly#readme",
|
"homepage": "https://gitlab.com/servezone/private/cloudly#readme",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cloud management",
|
"multi-cloud management",
|
||||||
"Docker Swarmkit",
|
"Docker Swarmkit",
|
||||||
"multi-cloud",
|
"container orchestration",
|
||||||
|
"cloud services",
|
||||||
|
"API integration",
|
||||||
|
"web interface",
|
||||||
|
"CLI",
|
||||||
|
"CI/CD integration",
|
||||||
|
"cloud providers",
|
||||||
"DigitalOcean",
|
"DigitalOcean",
|
||||||
"Hetzner Cloud",
|
"Hetzner Cloud",
|
||||||
"Cloudflare",
|
"Cloudflare",
|
||||||
"container orchestration",
|
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"node.js",
|
"Node.js",
|
||||||
"infrastructure automation",
|
"infrastructure automation",
|
||||||
"Cloudron",
|
|
||||||
"configuration management",
|
|
||||||
"SSL management",
|
|
||||||
"APIs",
|
|
||||||
"devOps",
|
"devOps",
|
||||||
"cloud integration"
|
"secret management",
|
||||||
]
|
"configuration management",
|
||||||
|
"task scheduling",
|
||||||
|
"logging",
|
||||||
|
"SSL management",
|
||||||
|
"system logging",
|
||||||
|
"cloud API client",
|
||||||
|
"frontend",
|
||||||
|
"backend",
|
||||||
|
"security"
|
||||||
|
],
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
13585
pnpm-lock.yaml
generated
13585
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- mongodb-memory-server
|
||||||
|
- puppeteer
|
@@ -0,0 +1,19 @@
|
|||||||
|
- This repository contains 4 projects around serve.zone
|
||||||
|
- the cloudly backend under ts/*
|
||||||
|
- the cloudly frontend under ts_web/*
|
||||||
|
- the api client under ts_apiclient
|
||||||
|
- the cli client under ts_cliclient
|
||||||
|
|
||||||
|
- the easiest method to spawn up a cloudly instance is to use the docker image:
|
||||||
|
`code.foss.global/serve.zone/cloudly:latest`
|
||||||
|
|
||||||
|
- Note: the exports are defined in the package.json.
|
||||||
|
- For now, cloud wise only the setup with cloudron and hetzner cloud is supported.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
- serve.zone is a monorepo containing multiple packages that work together to provide a complete container orchestration platform
|
||||||
|
- Uses Docker Swarm as the underlying container orchestration technology
|
||||||
|
- cloudly acts as the control plane providing API, web UI, and CLI interfaces
|
||||||
|
- coreflow runs inside Docker Swarm clusters to manage containers
|
||||||
|
- coretraffic runs on each node to handle traffic routing and SSL
|
||||||
|
- spark manages individual servers at the OS level
|
558
readme.md
558
readme.md
@@ -1,390 +1,242 @@
|
|||||||
# @serve.zone/cloudly
|
# @serve.zone/cloudly 🚀
|
||||||
A cloud manager utilizing Docker Swarmkit, designed for operations on Cloudron, and supports various cloud platforms like DigitalOcean, Hetzner Cloud, and Cloudflare.
|
|
||||||
|
**Multi-cloud orchestration made simple.** Manage containerized applications across cloud providers with Docker Swarmkit, featuring web dashboards, CLI tools, and powerful APIs.
|
||||||
|
|
||||||
|
## 🎯 What is Cloudly?
|
||||||
|
|
||||||
|
Cloudly is your command center for multi-cloud infrastructure. It abstracts away the complexity of managing resources across different cloud providers while giving you the power and flexibility you need for modern DevOps workflows.
|
||||||
|
|
||||||
|
### ✨ Key Features
|
||||||
|
|
||||||
|
- **🌐 Multi-Cloud Management** - Seamlessly orchestrate resources across Cloudflare, Hetzner, DigitalOcean and more
|
||||||
|
- **🐳 Docker Swarmkit Integration** - Native container orchestration with production-grade reliability
|
||||||
|
- **🔐 Secret Management** - Secure handling of credentials, API keys, and sensitive configuration
|
||||||
|
- **🎨 Web Dashboard** - Beautiful, responsive UI built with modern web components
|
||||||
|
- **⚡ CLI & API** - Full programmatic control through TypeScript/JavaScript APIs and command-line tools
|
||||||
|
- **🔒 SSL/TLS Automation** - Automatic certificate provisioning via Let's Encrypt
|
||||||
|
- **📊 Comprehensive Logging** - Built-in log aggregation and monitoring capabilities
|
||||||
|
- **🔄 Task Scheduling** - Automated workflows and background job management
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
## Install
|
|
||||||
To install `@serve.zone/cloudly`, run the following command in your terminal:
|
|
||||||
```bash
|
```bash
|
||||||
npm install @serve.zone/cloudly --save
|
# Install the main package
|
||||||
|
pnpm add @serve.zone/cloudly
|
||||||
|
|
||||||
|
# Or install the CLI globally
|
||||||
|
pnpm add -g @serve.zone/cli
|
||||||
|
|
||||||
|
# Or just the API client
|
||||||
|
pnpm add @serve.zone/api
|
||||||
```
|
```
|
||||||
This will install the package and add it to your project's `package.json` dependencies.
|
|
||||||
|
|
||||||
## Usage
|
### Basic Setup
|
||||||
`@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
|
```typescript
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
import { Cloudly } from '@serve.zone/cloudly';
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly();
|
// Initialize Cloudly with your configuration
|
||||||
```
|
const cloudly = new Cloudly({
|
||||||
|
cfToken: process.env.CLOUDFLARE_TOKEN,
|
||||||
The `Cloudly` class is the entry point to using the library features. It prepares the environment for configuring the cloud services.
|
hetznerToken: process.env.HETZNER_TOKEN,
|
||||||
|
environment: 'production',
|
||||||
#### Configuration
|
letsEncryptEmail: 'certs@example.com',
|
||||||
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.
|
publicUrl: 'cloudly.example.com',
|
||||||
|
publicPort: 443,
|
||||||
For example, to configure a connection to MongoDB, specify your MongoDB details as shown:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const myCloudlyConfig = {
|
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
mongoDbUrl: process.env.MONGODB_URL,
|
||||||
mongoDbName: 'myDatabase',
|
mongoDbName: 'cloudly',
|
||||||
mongoDbUser: 'myUser',
|
mongoDbUser: process.env.MONGODB_USER,
|
||||||
mongoDbPass: 'myPassword',
|
mongoDbPass: process.env.MONGODB_PASS,
|
||||||
},
|
}
|
||||||
// Additional configuration values...
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
// Start the platform
|
||||||
|
await cloudly.start();
|
||||||
|
console.log('🎉 Cloudly is running!');
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Managing Docker Swarmkit Cluster
|
## 🏗️ Architecture
|
||||||
Cloudly allows managing Docker Swarmkit clusters through an abstracted interface, simplifying operations such as deployment and scaling. Below are examples to demonstrate these capabilities.
|
|
||||||
|
|
||||||
### Example: Start a Cloudly Instance and Add a Cluster
|
Cloudly follows a modular architecture with clear separation of concerns:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Web Dashboard (UI) │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ API Layer (TypedRouter) │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ Core Managers │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ Cluster │ │ Image │ │ Secret │ ... │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ Cloud Connectors │
|
||||||
|
│ ┌──────────┐ ┌─────────┐ ┌──────────----┐ │
|
||||||
|
│ │Cloudflare│ │ Hetzner │ │ DigitalOcean │ │
|
||||||
|
│ └──────────┘ └─────────┘ └──────────----┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 Core Components
|
||||||
|
|
||||||
|
### 🔧 Managers
|
||||||
|
|
||||||
|
- **AuthManager** - Identity and access management
|
||||||
|
- **ClusterManager** - Docker Swarm cluster orchestration
|
||||||
|
- **ImageManager** - Container image lifecycle management
|
||||||
|
- **SecretManager** - Secure credential storage and distribution
|
||||||
|
- **ServerManager** - Cloud server provisioning and management
|
||||||
|
- **TaskManager** - Background job scheduling and execution
|
||||||
|
|
||||||
|
### 🔌 Connectors
|
||||||
|
|
||||||
|
- **CloudflareConnector** - DNS, CDN, and edge services
|
||||||
|
- **LetsencryptConnector** - Automatic SSL certificate provisioning
|
||||||
|
- **MongodbConnector** - Database persistence layer
|
||||||
|
- **HetznerConnector** - German cloud infrastructure
|
||||||
|
- **DigitalOceanConnector** - Developer-friendly cloud resources
|
||||||
|
|
||||||
|
## 📚 Usage Examples
|
||||||
|
|
||||||
|
### Managing Clusters
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly, ClusterManager } from '@serve.zone/cloudly';
|
// Create a new cluster
|
||||||
|
const cluster = await cloudly.clusterManager.createCluster({
|
||||||
|
name: 'production-cluster',
|
||||||
|
region: 'eu-central',
|
||||||
|
nodeCount: 3
|
||||||
|
});
|
||||||
|
|
||||||
async function main() {
|
// Deploy a service
|
||||||
const myCloudlyConfig = {
|
await cluster.deployService({
|
||||||
mongoDescriptor: {
|
name: 'api-service',
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
image: 'myapp:latest',
|
||||||
mongoDbName: 'myDatabase',
|
replicas: 3,
|
||||||
mongoDbUser: 'myUser',
|
ports: [{ published: 80, target: 3000 }]
|
||||||
mongoDbPass: 'myPassword',
|
});
|
||||||
},
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token'
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const clusterManager = myCloudlyInstance.clusterManager;
|
|
||||||
const newCluster = await clusterManager.storeCluster({
|
|
||||||
id: 'example_cluster_id',
|
|
||||||
data: {
|
|
||||||
name: 'example_cluster',
|
|
||||||
jumpCode: 'random_jump_code',
|
|
||||||
jumpCodeUsedAt: null,
|
|
||||||
secretKey: 'example_secret_key',
|
|
||||||
acmeInfo: null,
|
|
||||||
cloudlyUrl: 'https://example.com:8443',
|
|
||||||
servers: [],
|
|
||||||
sshKeys: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Cluster added:', newCluster);
|
|
||||||
}
|
|
||||||
main();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: Manage Cloudflare DNS Records
|
### Secret Management
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly, CloudflareConnector } from '@serve.zone/cloudly';
|
// Create a secret group
|
||||||
|
const secretGroup = await cloudly.secretManager.createSecretGroup({
|
||||||
|
name: 'api-credentials',
|
||||||
|
secrets: [
|
||||||
|
{ key: 'API_KEY', value: process.env.API_KEY },
|
||||||
|
{ key: 'DB_PASSWORD', value: process.env.DB_PASSWORD }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
async function manageDNSRecords() {
|
// Create a bundle for deployment
|
||||||
const myCloudlyConfig = {
|
const bundle = await cloudly.secretManager.createSecretBundle({
|
||||||
mongoDescriptor: {
|
name: 'production-secrets',
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
secretGroups: [secretGroup]
|
||||||
mongoDbName: 'myDatabase',
|
});
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token'
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const dnsInfo = {
|
|
||||||
zoneName: 'example.com',
|
|
||||||
recordName: 'sub.example.com',
|
|
||||||
recordType: 'A',
|
|
||||||
recordContent: '127.0.0.1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfConnector = myCloudlyInstance.cloudflareConnector.cloudflare;
|
|
||||||
const newRecord = await cfConnector.createDNSRecord(
|
|
||||||
dnsInfo.zoneName,
|
|
||||||
dnsInfo.recordName,
|
|
||||||
dnsInfo.recordType,
|
|
||||||
dnsInfo.recordContent
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('DNS Record created:', newRecord);
|
|
||||||
}
|
|
||||||
manageDNSRecords();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: Integrate with DigitalOcean
|
### DNS Management
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly, DigitalOceanConnector } from '@serve.zone/cloudly';
|
// Create DNS records via Cloudflare
|
||||||
|
const record = await cloudly.cloudflareConnector.createDNSRecord(
|
||||||
async function manageDroplet() {
|
'example.com',
|
||||||
const myCloudlyConfig = {
|
'api.example.com',
|
||||||
mongoDescriptor: {
|
'A',
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
'192.168.1.1'
|
||||||
mongoDbName: 'myDatabase',
|
);
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token'
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const doConnector = myCloudlyInstance.digitaloceanConnector;
|
|
||||||
const dropletInfo = {
|
|
||||||
name: 'example-droplet',
|
|
||||||
region: 'nyc3',
|
|
||||||
size: 's-1vcpu-1gb',
|
|
||||||
image: 'ubuntu-20-04-x64',
|
|
||||||
};
|
|
||||||
|
|
||||||
const newDroplet = await doConnector.createDroplet(
|
|
||||||
dropletInfo.name,
|
|
||||||
dropletInfo.region,
|
|
||||||
dropletInfo.size,
|
|
||||||
dropletInfo.image
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Droplet created:', newDroplet);
|
|
||||||
}
|
|
||||||
manageDroplet();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Cloudly Web Interface
|
### Web Dashboard
|
||||||
If your project includes a web interface to manage various sections like DNS, deployments, clusters, etc., you can use the provided elements and state management. Below is an example of setting up a dashboard using the components defined:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { commitinfo } from '../00_commitinfo_data.js';
|
import { html } from '@design.estate/dees-element';
|
||||||
import * as plugins from '../plugins.js';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
// Create a custom dashboard view
|
||||||
|
const dashboard = html`
|
||||||
|
<cloudly-dashboard>
|
||||||
|
<cloudly-view-clusters></cloudly-view-clusters>
|
||||||
|
<cloudly-view-dns></cloudly-view-dns>
|
||||||
|
<cloudly-view-images></cloudly-view-images>
|
||||||
|
</cloudly-dashboard>
|
||||||
|
`;
|
||||||
|
|
||||||
import {
|
document.body.appendChild(dashboard);
|
||||||
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,
|
|
||||||
},
|
|
||||||
] 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) {
|
|
||||||
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() {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This script sets up a cloud management dashboard for interacting with various cloud services seamlessly. It covers creating clusters, managing DNS records, handling cloud-provider-specific resources, and much more.
|
## 🛠️ CLI Usage
|
||||||
|
|
||||||
With the examples provided above, you should now have a good understanding of how to use `@serve.zone/cloudly` to manage your cloud infrastructure programmatically. For deeper insights and additional features, refer to the documentation relevant to specific modules and methods used in your application.
|
The CLI provides quick access to all Cloudly features:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login to your Cloudly instance
|
||||||
|
servezone login --url https://cloudly.example.com
|
||||||
|
|
||||||
|
# List clusters
|
||||||
|
servezone clusters list
|
||||||
|
|
||||||
|
# Deploy a service
|
||||||
|
servezone deploy --cluster prod --image myapp:latest
|
||||||
|
|
||||||
|
# Manage secrets
|
||||||
|
servezone secrets create --name api-key --value "secret123"
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
servezone logs --service api-service --follow
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Package Exports
|
||||||
|
|
||||||
|
This monorepo publishes multiple packages:
|
||||||
|
|
||||||
|
- **@serve.zone/cloudly** - Main orchestration platform
|
||||||
|
- **@serve.zone/api** - TypeScript/JavaScript API client
|
||||||
|
- **@serve.zone/cli** - Command-line interface
|
||||||
|
- **@serve.zone/interfaces** - Shared TypeScript interfaces
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
- **End-to-end encryption** for secrets
|
||||||
|
- **Role-based access control** (RBAC)
|
||||||
|
- **Automatic SSL/TLS** certificate management
|
||||||
|
- **Secure token-based authentication**
|
||||||
|
- **Audit logging** for compliance
|
||||||
|
|
||||||
|
## 🚢 Production Ready
|
||||||
|
|
||||||
|
Cloudly is battle-tested in production environments managing:
|
||||||
|
- High-traffic web applications
|
||||||
|
- Microservice architectures
|
||||||
|
- CI/CD pipelines
|
||||||
|
- Data processing workloads
|
||||||
|
- Real-time communication systems
|
||||||
|
|
||||||
|
## 🤝 Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://gitlab.com/servezone/private/cloudly.git
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start development mode
|
||||||
|
pnpm watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
For detailed documentation, API references, and guides, visit our [documentation site](https://cloudly.serve.zone).
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
52
test/helpers/cloudlyfactory.ts
Normal file
52
test/helpers/cloudlyfactory.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
|
import * as cloudly from '../../ts/index.js';
|
||||||
|
|
||||||
|
const stopFunctions: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
|
const tapToolsNodeMod = await import('@git.zone/tstest/tapbundle_node');
|
||||||
|
const smartmongo = await tapToolsNodeMod.tapNodeTools.createSmartmongo();
|
||||||
|
stopFunctions.push(async () => {
|
||||||
|
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
|
||||||
|
});
|
||||||
|
const smarts3 = await tapToolsNodeMod.tapNodeTools.createSmarts3();
|
||||||
|
await smarts3.createBucket('cloudly_test_bucket');
|
||||||
|
stopFunctions.push(async () => {
|
||||||
|
await smarts3.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testCloudlyConfig: cloudly.ICloudlyConfig = {
|
||||||
|
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
|
||||||
|
environment: 'integration',
|
||||||
|
letsEncryptEmail: 'test@serve.zone',
|
||||||
|
publicUrl: '127.0.0.1',
|
||||||
|
publicPort: '8080',
|
||||||
|
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
||||||
|
s3Descriptor: await smarts3.getS3Descriptor({
|
||||||
|
bucketName: 'cloudly_test_bucket'
|
||||||
|
}),
|
||||||
|
sslMode: 'none',
|
||||||
|
...(() => {
|
||||||
|
if (process.env.NPMCI_SECRET01) {
|
||||||
|
return {
|
||||||
|
hetznerToken: process.env.NPMCI_SECRET01,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await tapToolsNodeMod.tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
|
||||||
|
|
||||||
|
export const createCloudly = async () => {
|
||||||
|
const cloudlyInstance = new cloudly.Cloudly(testCloudlyConfig);
|
||||||
|
return cloudlyInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stopCloudly = async () => {
|
||||||
|
await Promise.all(stopFunctions.map((stopFunction) => stopFunction()));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnvVarOnDemand = async (envVarName: string) => {
|
||||||
|
return testQenv.getEnvVarOnDemand(envVarName);
|
||||||
|
};
|
9
test/helpers/docker.ts
Normal file
9
test/helpers/docker.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as smartstream from '@push.rocks/smartstream';
|
||||||
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
|
||||||
|
export const getAlpineImageReadableStream = async () => {
|
||||||
|
const currentDir = smartpath.get.dirnameFromImportMetaUrl(import.meta.url);
|
||||||
|
const imagePath = smartpath.join(currentDir, '../../.nogit/testfiles/alpine.tar');
|
||||||
|
const readableStream = smartstream.nodewebhelpers.createWebReadableStreamFromFile(imagePath);
|
||||||
|
return readableStream;
|
||||||
|
}
|
2
test/helpers/index.ts
Normal file
2
test/helpers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './cloudlyfactory.js';
|
||||||
|
export * from './docker.js';
|
138
test/test.apiclient.ts
Normal file
138
test/test.apiclient.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as helpers from './helpers/index.js';
|
||||||
|
|
||||||
|
import * as cloudly from '../ts/index.js';
|
||||||
|
import * as cloudlyApiClient from '../ts_apiclient/index.js';
|
||||||
|
import { Image } from '../ts_apiclient/classes.image.js';
|
||||||
|
|
||||||
|
let testCloudly: cloudly.Cloudly;
|
||||||
|
let testClient: cloudlyApiClient.CloudlyApiClient;
|
||||||
|
|
||||||
|
tap.preTask('should start cloudly', async () => {
|
||||||
|
testCloudly = await helpers.createCloudly();
|
||||||
|
await testCloudly.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.preTask('should create a new machine user for testing', async () => {
|
||||||
|
console.log('🔵 PreTask: Creating first machine user...');
|
||||||
|
const machineUser = new testCloudly.authManager.CUser();
|
||||||
|
machineUser.id = await testCloudly.authManager.CUser.getNewId();
|
||||||
|
console.log(` - User ID: ${machineUser.id}`);
|
||||||
|
machineUser.data = {
|
||||||
|
type: 'machine',
|
||||||
|
username: 'test',
|
||||||
|
password: 'test',
|
||||||
|
tokens: [{
|
||||||
|
token: 'test',
|
||||||
|
expiresAt: Date.now() + 3600 * 1000 * 24 * 365,
|
||||||
|
assignedRoles: ['admin'],
|
||||||
|
}],
|
||||||
|
role: 'admin',
|
||||||
|
};
|
||||||
|
console.log(` - Username: ${machineUser.data.username}`);
|
||||||
|
console.log(` - Role: ${machineUser.data.role}`);
|
||||||
|
console.log(` - Token: 'test'`);
|
||||||
|
console.log(` - Token roles: ${machineUser.data.tokens[0].assignedRoles}`);
|
||||||
|
await machineUser.save();
|
||||||
|
console.log('✅ PreTask: First machine user saved successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a new cloudlyApiClient', async () => {
|
||||||
|
console.log('🔵 Test: Creating CloudlyApiClient...');
|
||||||
|
testClient = new cloudlyApiClient.CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
||||||
|
});
|
||||||
|
console.log(` - URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
||||||
|
await testClient.start();
|
||||||
|
console.log('✅ CloudlyApiClient started successfully');
|
||||||
|
expect(testClient).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DEBUG: Check existing users', async () => {
|
||||||
|
console.log('🔍 DEBUG: Checking existing users in database...');
|
||||||
|
const allUsers = await testCloudly.authManager.CUser.getInstances({});
|
||||||
|
console.log(` - Total users found: ${allUsers.length}`);
|
||||||
|
for (const user of allUsers) {
|
||||||
|
console.log(` - User: ${user.data.username} (ID: ${user.id})`);
|
||||||
|
console.log(` - Type: ${user.data.type}`);
|
||||||
|
console.log(` - Role: ${user.data.role}`);
|
||||||
|
console.log(` - Tokens: ${user.data.tokens.length}`);
|
||||||
|
for (const token of user.data.tokens) {
|
||||||
|
console.log(` - Token: '${token.token}' | Roles: ${token.assignedRoles?.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should get an identity', async () => {
|
||||||
|
console.log('🔵 Test: Getting identity by token...');
|
||||||
|
console.log(` - Using token: 'test'`);
|
||||||
|
console.log(` - API URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await testClient.getIdentityByToken('test');
|
||||||
|
console.log('✅ Identity retrieved successfully:');
|
||||||
|
console.log(` - Identity exists: ${!!identity}`);
|
||||||
|
if (identity) {
|
||||||
|
console.log(` - Identity data:`, JSON.stringify(identity, null, 2));
|
||||||
|
}
|
||||||
|
expect(identity).toBeTruthy();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to get identity:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let image: Image;
|
||||||
|
tap.test('should create and upload an image', async () => {
|
||||||
|
console.log('🔵 Test: Creating and uploading image...');
|
||||||
|
console.log(` - Image name: 'test'`);
|
||||||
|
console.log(` - Image description: 'test'`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
image = await testClient.image.createImage({
|
||||||
|
name: 'test',
|
||||||
|
description: 'test'
|
||||||
|
});
|
||||||
|
console.log('✅ Image created successfully:');
|
||||||
|
console.log(` - Image ID: ${image?.id}`);
|
||||||
|
console.log(` - Image data:`, image);
|
||||||
|
expect(image).toBeInstanceOf(Image);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to create image:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tap.test('should upload an image version', async () => {
|
||||||
|
console.log('🔵 Test: Uploading image version...');
|
||||||
|
console.log(` - Version: 'v1.0.0'`);
|
||||||
|
console.log(` - Image exists: ${!!image}`);
|
||||||
|
console.log(` - Image ID: ${image?.id}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imageStream = await helpers.getAlpineImageReadableStream();
|
||||||
|
console.log(' - Image stream obtained successfully');
|
||||||
|
|
||||||
|
await image.pushImageVersion('v1.0.0', imageStream);
|
||||||
|
console.log('✅ Image version uploaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to upload image version:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should stop the apiclient', async (toolsArg) => {
|
||||||
|
await toolsArg.delayFor(10000);
|
||||||
|
await helpers.stopCloudly();
|
||||||
|
await testClient.stop();
|
||||||
|
await testCloudly.stop();
|
||||||
|
})
|
||||||
|
|
||||||
|
export default tap.start();
|
28
test/test.ts
28
test/test.ts
@@ -1,29 +1,11 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { Qenv } from '@push.rocks/qenv';
|
import * as helpers from './helpers/index.js';
|
||||||
const testQenv = new Qenv('./', './.nogit/');
|
|
||||||
process.env.TESTING_CLOUDLY = 'true';
|
|
||||||
|
|
||||||
delete process.env.CLI_CALL;
|
|
||||||
|
|
||||||
import * as cloudly from '../ts/index.js';
|
import * as cloudly from '../ts/index.js';
|
||||||
|
|
||||||
let testCloudly: cloudly.Cloudly;
|
let testCloudly: cloudly.Cloudly;
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
const cloudlyConfig: cloudly.ICloudlyConfig = {
|
testCloudly = await helpers.createCloudly();
|
||||||
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);
|
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,8 +14,10 @@ tap.test('should init cloudly', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should end the service', async (tools) => {
|
tap.test('should end the service', async (tools) => {
|
||||||
|
await tools.delayFor(5000);
|
||||||
|
await helpers.stopCloudly();
|
||||||
await testCloudly.stop();
|
await testCloudly.stop();
|
||||||
tools.delayFor(1000).then(() => process.exit())
|
tools.delayFor(1000).then(() => process.exit());
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* autocreated commitinfo by @pushrocks/commitinfo
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '1.1.0',
|
version: '5.0.5',
|
||||||
description: 'A cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.'
|
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
||||||
}
|
}
|
||||||
|
13
ts/00demo/demo.data.images.ts
Normal file
13
ts/00demo/demo.data.images.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export const demoImages: plugins.servezoneInterfaces.data.IImage[] = [
|
||||||
|
{
|
||||||
|
id: 'DemoImage1',
|
||||||
|
data: {
|
||||||
|
name: 'DemoImage1',
|
||||||
|
description: 'DemoImage1',
|
||||||
|
versions: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@@ -63,6 +63,8 @@ for (let i = 0; i < demoSecretGroups.length; i++) {
|
|||||||
id: `configBundleId${i + 1}`,
|
id: `configBundleId${i + 1}`,
|
||||||
data: {
|
data: {
|
||||||
name: `Demo Config Bundle ${i + 1}`,
|
name: `Demo Config Bundle ${i + 1}`,
|
||||||
|
imageClaims: [],
|
||||||
|
type: 'external',
|
||||||
description: 'Demo Purpose',
|
description: 'Demo Purpose',
|
||||||
includedSecretGroupIds: [secretGroup.id],
|
includedSecretGroupIds: [secretGroup.id],
|
||||||
includedTags: secretGroup.data.tags,
|
includedTags: secretGroup.data.tags,
|
20
ts/00demo/demo.data.users.ts
Normal file
20
ts/00demo/demo.data.users.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
|
||||||
|
export const getUsers = async (cloudlyRef: Cloudly) => {
|
||||||
|
const users: plugins.servezoneInterfaces.data.IUser[] = [];
|
||||||
|
const envAdminUser = await cloudlyRef.config.appData.waitForAndGetKey('servezoneAdminaccount');
|
||||||
|
if (envAdminUser) {
|
||||||
|
users.push({
|
||||||
|
id: 'envadmin',
|
||||||
|
data: {
|
||||||
|
type: 'human',
|
||||||
|
username: envAdminUser.split(':')[0],
|
||||||
|
password: envAdminUser.split(':')[1],
|
||||||
|
role: 'admin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
};
|
@@ -43,9 +43,23 @@ export const installDemoData = async (cloudlyRef: Cloudly) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const demoDataUsers = await import('./demo.data.users.js');
|
const demoDataUsers = await import('./demo.data.users.js');
|
||||||
for (const user of demoDataUsers.users) {
|
for (const user of await demoDataUsers.getUsers(cloudlyRef)) {
|
||||||
const userInstance = new cloudlyRef.authManager.CUser();
|
const userInstance = new cloudlyRef.authManager.CUser();
|
||||||
Object.assign(userInstance, user);
|
Object.assign(userInstance, user);
|
||||||
await userInstance.save();
|
await userInstance.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// IMAGES
|
||||||
|
const images = await cloudlyRef.imageManager.CImage.getInstances({});
|
||||||
|
for (const image of images) {
|
||||||
|
await image.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
const demoDataImages = await import('./demo.data.images.js');
|
||||||
|
for (const image of demoDataImages.demoImages) {
|
||||||
|
const imageInstance = new cloudlyRef.imageManager.CImage();
|
||||||
|
Object.assign(imageInstance, image);
|
||||||
|
await imageInstance.save();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -15,11 +15,12 @@ import { MongodbConnector } from './connector.mongodb/connector.js';
|
|||||||
|
|
||||||
// processes
|
// processes
|
||||||
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
|
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
|
||||||
import { ClusterManager } from './manager.cluster/clustermanager.js';
|
import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
|
||||||
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
|
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
|
||||||
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'
|
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
|
||||||
import { CloudlyServerManager } from './manager.server/servermanager.js';
|
import { CloudlyServerManager } from './manager.server/classes.servermanager.js';
|
||||||
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
||||||
|
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
||||||
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
||||||
@@ -54,13 +55,16 @@ export class Cloudly {
|
|||||||
public clusterManager: ClusterManager;
|
public clusterManager: ClusterManager;
|
||||||
public coreflowManager: CloudlyCoreflowManager;
|
public coreflowManager: CloudlyCoreflowManager;
|
||||||
public externalApiManager: ExternalApiManager;
|
public externalApiManager: ExternalApiManager;
|
||||||
|
public externalRegistryManager: ExternalRegistryManager;
|
||||||
public imageManager: ImageManager;
|
public imageManager: ImageManager;
|
||||||
public taskManager: CloudlyTaskmanager;
|
public taskManager: CloudlyTaskmanager;
|
||||||
public serverManager: CloudlyServerManager;
|
public serverManager: CloudlyServerManager;
|
||||||
|
|
||||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
|
|
||||||
constructor() {
|
private configOptions: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||||
|
constructor(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||||
|
this.configOptions = configArg;
|
||||||
this.cloudlyInfo = new CloudlyInfo(this);
|
this.cloudlyInfo = new CloudlyInfo(this);
|
||||||
this.config = new CloudlyConfig(this);
|
this.config = new CloudlyConfig(this);
|
||||||
|
|
||||||
@@ -78,6 +82,7 @@ export class Cloudly {
|
|||||||
this.clusterManager = new ClusterManager(this);
|
this.clusterManager = new ClusterManager(this);
|
||||||
this.coreflowManager = new CloudlyCoreflowManager(this);
|
this.coreflowManager = new CloudlyCoreflowManager(this);
|
||||||
this.externalApiManager = new ExternalApiManager(this);
|
this.externalApiManager = new ExternalApiManager(this);
|
||||||
|
this.externalRegistryManager = new ExternalRegistryManager(this);
|
||||||
this.imageManager = new ImageManager(this);
|
this.imageManager = new ImageManager(this);
|
||||||
this.taskManager = new CloudlyTaskmanager(this);
|
this.taskManager = new CloudlyTaskmanager(this);
|
||||||
this.secretManager = new CloudlySecretManager(this);
|
this.secretManager = new CloudlySecretManager(this);
|
||||||
@@ -90,7 +95,7 @@ export class Cloudly {
|
|||||||
*/
|
*/
|
||||||
public async start() {
|
public async start() {
|
||||||
// config
|
// config
|
||||||
await this.config.init();
|
await this.config.init(this.configOptions);
|
||||||
|
|
||||||
// manageers
|
// manageers
|
||||||
await this.authManager.start();
|
await this.authManager.start();
|
||||||
|
@@ -9,18 +9,16 @@ import type { Cloudly } from './classes.cloudly.js';
|
|||||||
export class CloudlyConfig {
|
export class CloudlyConfig {
|
||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
||||||
public data: plugins.servezoneInterfaces.data.ICloudlyConfig
|
public data: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||||
|
|
||||||
// authentication and settings
|
|
||||||
public smartjwtInstance: plugins.smartjwt.SmartJwt;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||||
this.appData = await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>({
|
this.appData =
|
||||||
|
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
||||||
|
{
|
||||||
envMapping: {
|
envMapping: {
|
||||||
cfToken: 'CF_TOKEN',
|
cfToken: 'CF_TOKEN',
|
||||||
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
||||||
@@ -31,18 +29,21 @@ export class CloudlyConfig {
|
|||||||
publicPort: 'SERVEZONE_PORT',
|
publicPort: 'SERVEZONE_PORT',
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbUrl: 'MONGODB_URL',
|
mongoDbUrl: 'MONGODB_URL',
|
||||||
mongoDbName: 'MONGODB_DATABASE',
|
mongoDbName: 'MONGODB_NAME',
|
||||||
mongoDbUser: 'MONGODB_USER',
|
mongoDbUser: 'MONGODB_USER',
|
||||||
mongoDbPass: 'MONGODB_PASSWORD',
|
mongoDbPass: 'MONGODB_PASS',
|
||||||
},
|
},
|
||||||
s3Descriptor: {
|
s3Descriptor: {
|
||||||
endpoint: 'S3_ENDPOINT',
|
endpoint: 'S3_ENDPOINT',
|
||||||
accessKey: 'S3_ACCESSKEY',
|
accessKey: 'S3_ACCESSKEY',
|
||||||
accessSecret: 'S3_SECRETKEY',
|
accessSecret: 'S3_SECRETKEY',
|
||||||
port: 'S3_PORT', // Note: This will remain as a string. Ensure to parse it to an integer where it's used.
|
port: 'S3_PORT', // Note: This will remain as a string. Ensure to parse it to an integer where it's used.
|
||||||
useSsl: true,
|
useSsl: 'boolean:S3_USESSL' as any as boolean,
|
||||||
|
bucketName: 'S3_BUCKET'
|
||||||
},
|
},
|
||||||
sslMode: 'SERVEZONE_SSLMODE' as plugins.servezoneInterfaces.data.ICloudlyConfig['sslMode'],
|
sslMode:
|
||||||
|
'SERVEZONE_SSLMODE' as plugins.servezoneInterfaces.data.ICloudlyConfig['sslMode'],
|
||||||
|
servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT',
|
||||||
},
|
},
|
||||||
requiredKeys: [
|
requiredKeys: [
|
||||||
'cfToken',
|
'cfToken',
|
||||||
@@ -54,7 +55,9 @@ export class CloudlyConfig {
|
|||||||
'environment',
|
'environment',
|
||||||
'mongoDescriptor',
|
'mongoDescriptor',
|
||||||
],
|
],
|
||||||
});
|
overwriteObject: configArg,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const kvStore = await this.appData.getKvStore();
|
const kvStore = await this.appData.getKvStore();
|
||||||
|
|
||||||
|
@@ -10,7 +10,8 @@ export class CloudlyServer {
|
|||||||
/**
|
/**
|
||||||
* a reference to the cloudly instance
|
* a reference to the cloudly instance
|
||||||
*/
|
*/
|
||||||
private cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
|
public additionalHandlers: plugins.typedserver.servertools.Handler[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the smartexpress server handling the actual requests
|
* the smartexpress server handling the actual requests
|
||||||
@@ -37,18 +38,24 @@ export class CloudlyServer {
|
|||||||
* init the reception instance
|
* init the reception instance
|
||||||
*/
|
*/
|
||||||
public async start() {
|
public async start() {
|
||||||
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`)
|
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`);
|
||||||
let sslCert: plugins.smartacme.Cert;
|
let sslCert: plugins.smartacme.Cert;
|
||||||
|
|
||||||
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
|
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
|
||||||
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`)
|
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`);
|
||||||
logger.log('info', `This might take 10 minutes...`)
|
logger.log('info', `This might take 10 minutes...`);
|
||||||
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
||||||
this.cloudlyRef.config.data.publicUrl
|
this.cloudlyRef.config.data.publicUrl,
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
'success',
|
||||||
|
`Successfully obtained certificate for cloudly domain ${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') {
|
} 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.`)
|
logger.log(
|
||||||
|
'info',
|
||||||
|
`Using external certificate for ssl mode, meaning cloudly is not in charge of ssl termination.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRequestGuardData {
|
interface IRequestGuardData {
|
||||||
@@ -72,11 +79,13 @@ export class CloudlyServer {
|
|||||||
this.typedServer = new plugins.typedserver.TypedServer({
|
this.typedServer = new plugins.typedserver.TypedServer({
|
||||||
cors: true,
|
cors: true,
|
||||||
forceSsl: false,
|
forceSsl: false,
|
||||||
port: this.cloudlyRef.config.data.publicPort,
|
port: this.cloudlyRef.config.data.publicPort,
|
||||||
...(sslCert ? {
|
...(sslCert
|
||||||
|
? {
|
||||||
privateKey: sslCert.privateKey,
|
privateKey: sslCert.privateKey,
|
||||||
publicKey: sslCert.publicKey,
|
publicKey: sslCert.publicKey,
|
||||||
} : {}),
|
}
|
||||||
|
: {}),
|
||||||
injectReload: true,
|
injectReload: true,
|
||||||
serveDir: paths.distServeDir,
|
serveDir: paths.distServeDir,
|
||||||
watch: true,
|
watch: true,
|
||||||
@@ -84,6 +93,10 @@ export class CloudlyServer {
|
|||||||
preferredCompressionMethod: 'gzip',
|
preferredCompressionMethod: 'gzip',
|
||||||
});
|
});
|
||||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.typedServer.server.addRoute(
|
||||||
|
'/curlfresh/:scriptname',
|
||||||
|
this.cloudlyRef.serverManager.curlfreshInstance.handler,
|
||||||
|
);
|
||||||
await this.typedServer.start();
|
await this.typedServer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,17 +24,20 @@ export class LetsencryptConnector {
|
|||||||
environment: this.cloudlyRef.config.data.environment,
|
environment: this.cloudlyRef.config.data.environment,
|
||||||
setChallenge: async (dnsChallenge) => {
|
setChallenge: async (dnsChallenge) => {
|
||||||
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeSetDnsChallenge(
|
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeSetDnsChallenge(
|
||||||
dnsChallenge
|
dnsChallenge,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
removeChallenge: async (dnsChallenge) => {
|
removeChallenge: async (dnsChallenge) => {
|
||||||
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeRemoveDnsChallenge(
|
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeRemoveDnsChallenge(
|
||||||
dnsChallenge
|
dnsChallenge,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor,
|
mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor,
|
||||||
});
|
});
|
||||||
await this.smartacme.init();
|
await this.smartacme.start().catch((err) => {
|
||||||
|
console.error('error in init', err);
|
||||||
|
console.log(`trying again in a few minutes`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,7 +11,9 @@ export class MongodbConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.cloudlyRef.config.data.mongoDescriptor);
|
this.smartdataDb = new plugins.smartdata.SmartdataDb(
|
||||||
|
this.cloudlyRef.config.data.mongoDescriptor,
|
||||||
|
);
|
||||||
await this.smartdataDb.init();
|
await this.smartdataDb.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
export const users = [
|
|
||||||
{
|
|
||||||
id: 'user1',
|
|
||||||
data: {
|
|
||||||
username: 'admin',
|
|
||||||
password: 'password',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@@ -7,7 +7,6 @@ import { logger } from './logger.js';
|
|||||||
const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true);
|
const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true);
|
||||||
early.stop();
|
early.stop();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* starts the cloudly instance
|
* starts the cloudly instance
|
||||||
*/
|
*/
|
||||||
@@ -17,12 +16,14 @@ const runCli = async () => {
|
|||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
'info',
|
'info',
|
||||||
`running in environment ${await cloudlyQenv.getEnvVarOnDemand('SERVEZONE_ENVIRONMENT')}`
|
`running in environment ${await cloudlyQenv.getEnvVarOnDemand('SERVEZONE_ENVIRONMENT')}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await cloudlyInstance.start();
|
await cloudlyInstance.start();
|
||||||
const demoMod = await import('./demo/index.js');
|
const demoMod = await import('./00demo/index.js');
|
||||||
demoMod.installDemoData(cloudlyInstance);
|
demoMod.installDemoData(cloudlyInstance);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { runCli, Cloudly };
|
export { runCli, Cloudly };
|
||||||
|
type ICloudlyConfig = plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||||
|
export { type ICloudlyConfig };
|
||||||
|
@@ -9,8 +9,8 @@ export const logger = new plugins.smartlog.Smartlog({
|
|||||||
zone: null,
|
zone: null,
|
||||||
companyunit: null,
|
companyunit: null,
|
||||||
containerName: null,
|
containerName: null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
logger.enableConsole({
|
logger.enableConsole({
|
||||||
captureAll: false
|
captureAll: false,
|
||||||
});
|
});
|
||||||
|
@@ -5,14 +5,14 @@ import { logger } from '../logger.js';
|
|||||||
import { Authorization } from './classes.authorization.js';
|
import { Authorization } from './classes.authorization.js';
|
||||||
import { User } from './classes.user.js';
|
import { User } from './classes.user.js';
|
||||||
|
|
||||||
|
|
||||||
export interface IJwtData {
|
export interface IJwtData {
|
||||||
userId: string;
|
userId: string;
|
||||||
status: 'loggedIn' | 'loggedOut';
|
status: 'loggedIn' | 'loggedOut';
|
||||||
|
expiresAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CloudlyAuthManager {
|
export class CloudlyAuthManager {
|
||||||
cloudlyRef: Cloudly
|
cloudlyRef: Cloudly;
|
||||||
public get db() {
|
public get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
}
|
}
|
||||||
@@ -27,17 +27,24 @@ export class CloudlyAuthManager {
|
|||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createNewSecureToken() {
|
||||||
|
return plugins.smartunique.uniSimple('secureToken', 64);
|
||||||
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
// lets setup the smartjwtInstance
|
// lets setup the smartjwtInstance
|
||||||
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
||||||
|
await this.smartjwtInstance.init();
|
||||||
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
|
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
|
||||||
|
|
||||||
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
|
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = (await kvStore.readKey(
|
||||||
|
'jwtKeypair',
|
||||||
|
)) as plugins.tsclass.network.IJwtKeypair;
|
||||||
|
|
||||||
if (!existingJwtKeys) {
|
if (!existingJwtKeys) {
|
||||||
await this.smartjwtInstance.createNewKeyPair();
|
await this.smartjwtInstance.createNewKeyPair();
|
||||||
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
|
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
|
||||||
await kvStore.writeKey('jwtKeys', newJwtKeys);
|
await kvStore.writeKey('jwtKeypair', newJwtKeys);
|
||||||
} else {
|
} else {
|
||||||
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
|
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
|
||||||
}
|
}
|
||||||
@@ -47,30 +54,84 @@ export class CloudlyAuthManager {
|
|||||||
'adminLoginWithUsernameAndPassword',
|
'adminLoginWithUsernameAndPassword',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
let jwt: string;
|
let jwt: string;
|
||||||
|
let expiresAtTimestamp: number = Date.now() + 3600 * 1000 * 24 * 7;
|
||||||
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
|
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.log('warn', 'login failed');
|
logger.log('warn', 'login failed');
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('login failed');
|
||||||
} else {
|
} else {
|
||||||
jwt = await this.cloudlyRef.config.smartjwtInstance.createJWT({
|
jwt = await this.smartjwtInstance.createJWT({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
status: 'loggedIn',
|
status: 'loggedIn',
|
||||||
|
expiresAt: expiresAtTimestamp,
|
||||||
});
|
});
|
||||||
logger.log('success', 'login successful');
|
logger.log('success', 'login successful');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
identity: {
|
||||||
jwt,
|
jwt,
|
||||||
|
userId: user.id,
|
||||||
|
name: user.data.username,
|
||||||
|
expiresAt: expiresAtTimestamp,
|
||||||
|
role: user.data.role,
|
||||||
|
type: user.data.type,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop () {}
|
public async stop() {}
|
||||||
|
|
||||||
public adminJwtGuard = new plugins.smartguard.Guard<{jwt: string}>(async (dataArg) => {
|
public validIdentityGuard = new plugins.smartguard.Guard<{
|
||||||
const jwt = dataArg.jwt;
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
const jwtData: IJwtData = await this.cloudlyRef.config.smartjwtInstance.verifyJWTAndGetData(jwt);
|
}>(
|
||||||
const user = await this.CUser.getInstance({id: jwtData.userId});
|
async (dataArg) => {
|
||||||
return user.data.role === 'admin';
|
const jwt = dataArg.identity.jwt;
|
||||||
})
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
||||||
|
const expired = jwtData.expiresAt < Date.now();
|
||||||
|
plugins.smartexpect
|
||||||
|
.expect(jwtData.status)
|
||||||
|
.setFailMessage('user not logged in')
|
||||||
|
.toEqual('loggedIn');
|
||||||
|
plugins.smartexpect.expect(expired).setFailMessage(`jwt expired`).toBeFalse();
|
||||||
|
plugins.smartexpect
|
||||||
|
.expect(dataArg.identity.expiresAt)
|
||||||
|
.setFailMessage(
|
||||||
|
`expiresAt >>identity valid until:${dataArg.identity.expiresAt}, but jwt says: ${jwtData.expiresAt}<< has been tampered with`,
|
||||||
|
)
|
||||||
|
.toEqual(jwtData.expiresAt);
|
||||||
|
plugins.smartexpect
|
||||||
|
.expect(dataArg.identity.userId)
|
||||||
|
.setFailMessage('userId has been tampered with')
|
||||||
|
.toEqual(jwtData.userId);
|
||||||
|
if (expired) {
|
||||||
|
throw new Error('identity is expired');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failedHint: 'identity is not valid.',
|
||||||
|
name: 'validIdentityGuard',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
public adminIdentityGuard = new plugins.smartguard.Guard<{
|
||||||
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
|
}>(
|
||||||
|
async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
|
||||||
|
const jwt = dataArg.identity.jwt;
|
||||||
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
||||||
|
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
||||||
|
const isAdminBool = user.data.role === 'admin';
|
||||||
|
console.log(`user is admin: ${isAdminBool}`);
|
||||||
|
return isAdminBool;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failedHint: 'user is not admin.',
|
||||||
|
name: 'adminIdentityGuard',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
@@ -1,6 +1,4 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
@plugins.smartdata.managed()
|
@plugins.smartdata.managed()
|
||||||
export class Authorization extends plugins.smartdata.SmartDataDbDoc<Authorization, Authorization> {
|
export class Authorization extends plugins.smartdata.SmartDataDbDoc<Authorization, Authorization> {}
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -1,24 +1,52 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
@plugins.smartdata.managed()
|
@plugins.smartdata.managed()
|
||||||
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> {
|
export class User extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
User,
|
||||||
|
plugins.servezoneInterfaces.data.IUser
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* creates a machine user
|
||||||
|
*/
|
||||||
|
public static async createMachineUser(userNameArg: string, roleArg: 'api' | 'cluster') {
|
||||||
|
const user = new User();
|
||||||
|
user.id = await User.getNewId();
|
||||||
|
user.data = {
|
||||||
|
type: 'machine',
|
||||||
|
username: userNameArg,
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
token: 'machineUser',
|
||||||
|
expiresAt: Date.now() + 3600 * 1000 * 24 * 365,
|
||||||
|
assignedRoles: ['admin'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: 'api',
|
||||||
|
};
|
||||||
|
await user.save();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
public static async findUserByUsernameAndPassword(usernameArg: string, passwordArg: string) {
|
public static async findUserByUsernameAndPassword(usernameArg: string, passwordArg: string) {
|
||||||
return await User.getInstance({
|
return await User.getInstance({
|
||||||
data: {
|
data: {
|
||||||
username: usernameArg,
|
username: usernameArg,
|
||||||
password: passwordArg,
|
password: passwordArg,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(optionsArg?: plugins.servezoneInterfaces.data.IUser) {
|
||||||
|
super();
|
||||||
|
if (optionsArg) {
|
||||||
|
Object.assign(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: {
|
public data: plugins.servezoneInterfaces.data.IUser['data'];
|
||||||
role: 'admin' | 'user';
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
3
ts/manager.cert/cert.ts
Normal file
3
ts/manager.cert/cert.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export class Cert extends plugins.smartdata.SmartDataDbDoc<Cert, Cert> {}
|
14
ts/manager.cert/certmanager.ts
Normal file
14
ts/manager.cert/certmanager.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export class CertManager {
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
public get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(cloudly: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudly;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,11 +4,12 @@ import * as plugins from '../plugins.js';
|
|||||||
* cluster defines a swarmkit cluster
|
* cluster defines a swarmkit cluster
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.managed()
|
@plugins.smartdata.managed()
|
||||||
export class Cluster extends plugins.smartdata.SmartDataDbDoc<Cluster, plugins.servezoneInterfaces.data.ICluster> {
|
export class Cluster extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Cluster,
|
||||||
|
plugins.servezoneInterfaces.data.ICluster
|
||||||
|
> {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static async fromConfigObject(
|
public static async fromConfigObject(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
||||||
configObjectArg: plugins.servezoneInterfaces.data.ICluster
|
|
||||||
) {
|
|
||||||
const newCluster = new Cluster();
|
const newCluster = new Cluster();
|
||||||
Object.assign(newCluster, configObjectArg);
|
Object.assign(newCluster, configObjectArg);
|
||||||
return newCluster;
|
return newCluster;
|
@@ -3,7 +3,8 @@ import * as paths from '../paths.js';
|
|||||||
import { Cloudly } from '../classes.cloudly.js';
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
import { Cluster } from './cluster.js';
|
import { Cluster } from './classes.cluster.js';
|
||||||
|
import { data } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
export class ClusterManager {
|
export class ClusterManager {
|
||||||
public ready = plugins.smartpromise.defer();
|
public ready = plugins.smartpromise.defer();
|
||||||
@@ -22,13 +23,12 @@ export class ClusterManager {
|
|||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
||||||
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
|
||||||
const cluster = await this.storeCluster({
|
// TODO: guards
|
||||||
|
const cluster = await this.createCluster({
|
||||||
id: plugins.smartunique.uniSimple('cluster'),
|
id: plugins.smartunique.uniSimple('cluster'),
|
||||||
data: {
|
data: {
|
||||||
|
userId: null, // this is created by the createCluster method
|
||||||
name: dataArg.clusterName,
|
name: dataArg.clusterName,
|
||||||
jumpCode: plugins.smartunique.uniSimple('cluster'),
|
|
||||||
jumpCodeUsedAt: null,
|
|
||||||
secretKey: plugins.smartunique.shortId(16),
|
|
||||||
acmeInfo: null,
|
acmeInfo: null,
|
||||||
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
||||||
servers: [],
|
servers: [],
|
||||||
@@ -38,21 +38,32 @@ export class ClusterManager {
|
|||||||
console.log(await cluster.createSavableObject());
|
console.log(await cluster.createSavableObject());
|
||||||
this.cloudlyRef.serverManager.ensureServerInfrastructure();
|
this.cloudlyRef.serverManager.ensureServerInfrastructure();
|
||||||
return {
|
return {
|
||||||
clusterConfig: await cluster.createSavableObject(),
|
cluster: await cluster.createSavableObject(),
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_GetAllClusters>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
||||||
new plugins.typedrequest.TypedHandler('getAllClusters', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg) => {
|
||||||
// TODO: do authentication here
|
// TODO: do authentication here
|
||||||
const clusters = await this.getAllClusters();
|
const clusters = await this.getAllClusters();
|
||||||
return {
|
return {
|
||||||
clusters: await Promise.all(
|
clusters: await Promise.all(
|
||||||
clusters.map((clusterArg) => clusterArg.createSavableObject())
|
clusters.map((clusterArg) => clusterArg.createSavableObject()),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete cluster
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
|
||||||
|
new plugins.typedrequest.TypedHandler('deleteClusterById', async (reqDataArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqDataArg);
|
||||||
|
await this.deleteCluster(reqDataArg.clusterId);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,25 +81,22 @@ export class ClusterManager {
|
|||||||
// TODO: implement getclusterConfigByServerIp
|
// TODO: implement getclusterConfigByServerIp
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClusterConfigBy_JumpCode(jumpCodeArg: string) {
|
public async getClusterBy_UserId(userIdArg: string) {
|
||||||
await this.ready.promise;
|
await this.ready.promise;
|
||||||
|
|
||||||
return await Cluster.getInstance({
|
return await Cluster.getInstance({
|
||||||
data: {
|
data: {
|
||||||
jumpCode: jumpCodeArg,
|
userId: userIdArg,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClusterConfigBy_ClusterIdentifier(
|
public async getClusterBy_Identity(clusterIdentity: plugins.servezoneInterfaces.data.IIdentity) {
|
||||||
clusterIdentifier: plugins.servezoneInterfaces.data.IClusterIdentifier
|
|
||||||
) {
|
|
||||||
await this.ready.promise;
|
await this.ready.promise;
|
||||||
|
|
||||||
return await Cluster.getInstance({
|
return await Cluster.getInstance({
|
||||||
data: {
|
data: {
|
||||||
name: clusterIdentifier.clusterName,
|
userId: clusterIdentity.userId,
|
||||||
secretKey: clusterIdentifier.secretKey,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,18 +122,39 @@ export class ClusterManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* allows storage of a config
|
* creates a cluster (and a new user for it) and saves it
|
||||||
* @param configName
|
* @param configName
|
||||||
* @param configObjectArg
|
* @param configObjectArg
|
||||||
*/
|
*/
|
||||||
public async storeCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
public async createCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
||||||
let clusterInstance = await Cluster.getInstance({ id: configObjectArg.id });
|
// TODO: guards
|
||||||
if (!clusterInstance) {
|
// lets create the cluster user
|
||||||
clusterInstance = await Cluster.fromConfigObject(configObjectArg);
|
const clusterUser = new this.cloudlyRef.authManager.CUser({
|
||||||
} else {
|
id: await this.cloudlyRef.authManager.CUser.getNewId(),
|
||||||
Object.assign(clusterInstance, configObjectArg);
|
data: {
|
||||||
}
|
role: 'cluster',
|
||||||
|
type: 'machine',
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
expiresAt: Date.now() + 3600 * 1000 * 24 * 365,
|
||||||
|
assignedRoles: ['cluster'],
|
||||||
|
token: await this.cloudlyRef.authManager.createNewSecureToken(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await clusterUser.save();
|
||||||
|
Object.assign(configObjectArg, {
|
||||||
|
userId: clusterUser.id,
|
||||||
|
});
|
||||||
|
const clusterInstance = await Cluster.fromConfigObject(configObjectArg);
|
||||||
await clusterInstance.save();
|
await clusterInstance.save();
|
||||||
return clusterInstance;
|
return clusterInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteCluster(clusterId: string) {
|
||||||
|
await this.ready.promise;
|
||||||
|
const clusterInstance = await Cluster.getInstance({ id: clusterId });
|
||||||
|
await clusterInstance.delete();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { Cloudly } from '../classes.cloudly.js';
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import type { Cluster } from '../manager.cluster/classes.cluster.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* in charge of talking to coreflow services on clusters
|
* in charge of talking to coreflow services on clusters
|
||||||
@@ -13,21 +14,50 @@ export class CloudlyCoreflowManager {
|
|||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||||
|
|
||||||
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>(
|
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByToken>(
|
||||||
new plugins.typedrequest.TypedHandler('getIdentityByJumpCode', async (requestData) => {
|
new plugins.typedrequest.TypedHandler('getIdentityByToken', async (requestData) => {
|
||||||
const clusterConfig =
|
// Use getInstance with $elemMatch for querying nested arrays
|
||||||
await this.cloudlyRef.clusterManager.getClusterConfigBy_JumpCode(
|
const user = await this.cloudlyRef.authManager.CUser.getInstance({
|
||||||
requestData.jumpCode
|
data: {
|
||||||
|
tokens: {
|
||||||
|
$elemMatch: { token: requestData.token },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
'The supplied token is not valid. No matching user found.'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!clusterConfig) {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError('The supplied jumpCode is not valid.');
|
|
||||||
}
|
}
|
||||||
|
if (user.data.type !== 'machine') {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
'The supplied token is not valid. The user is not a machine.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cluster: Cluster;
|
||||||
|
if (user.data.role === 'cluster') {
|
||||||
|
cluster = await this.cloudlyRef.clusterManager.getClusterBy_UserId(user.id);
|
||||||
|
}
|
||||||
|
const expiryTimestamp = Date.now() + 3600 * 1000 * 24 * 365;
|
||||||
return {
|
return {
|
||||||
clusterIdentifier: {
|
identity: {
|
||||||
clusterName: clusterConfig.data.name,
|
name: user.data.username,
|
||||||
secretKey: clusterConfig.data.secretKey,
|
role: user.data.role,
|
||||||
|
type: 'machine', // if someone authenticates by token, they are a machine, no matter what.
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: expiryTimestamp,
|
||||||
|
...(cluster
|
||||||
|
? {
|
||||||
|
clusterId: cluster.id,
|
||||||
|
clusterName: cluster.data.name,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
jwt: await this.cloudlyRef.authManager.smartjwtInstance.createJWT({
|
||||||
|
status: 'loggedIn',
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: expiryTimestamp,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -38,16 +68,14 @@ export class CloudlyCoreflowManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig>(
|
||||||
'getClusterConfig',
|
'getClusterConfig',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const clusterIdentifier = dataArg.clusterIdentifier;
|
const identity = dataArg.identity;
|
||||||
console.log('trying to get clusterConfigSet');
|
console.log('trying to get clusterConfigSet');
|
||||||
console.log(dataArg);
|
console.log(dataArg);
|
||||||
const clusterConfigSet =
|
const cluster = await this.cloudlyRef.clusterManager.getClusterBy_Identity(identity);
|
||||||
await this.cloudlyRef.clusterManager.getClusterConfigBy_ClusterIdentifier(
|
|
||||||
clusterIdentifier
|
|
||||||
);
|
|
||||||
console.log('got cluster config and sending it back to coreflow');
|
console.log('got cluster config and sending it back to coreflow');
|
||||||
return {
|
return {
|
||||||
configData: await clusterConfigSet.createSavableObject()
|
configData: await cluster.createSavableObject(),
|
||||||
|
services: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -55,14 +83,14 @@ export class CloudlyCoreflowManager {
|
|||||||
|
|
||||||
// lets enable getting of certificates
|
// lets enable getting of certificates
|
||||||
this.typedRouter.addTypedHandler(
|
this.typedRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetSslCertificate>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetCertificateForDomain>(
|
||||||
'getSslCertificate',
|
'getCertificateForDomain',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
console.log(`got request for certificate ${dataArg.requiredCertName}`);
|
console.log(`incoming API request for certificate ${dataArg.domainName}`);
|
||||||
const cert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
const cert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
||||||
dataArg.requiredCertName
|
dataArg.domainName
|
||||||
);
|
);
|
||||||
console.log(`got certificate ready for reponse ${dataArg.requiredCertName}`);
|
console.log(`got certificate ready for reponse ${dataArg.domainName}`);
|
||||||
return {
|
return {
|
||||||
certificate: await cert.createSavableObject(),
|
certificate: await cert.createSavableObject(),
|
||||||
};
|
};
|
||||||
|
40
ts/manager.externalregistry/classes.externalregistry.ts
Normal file
40
ts/manager.externalregistry/classes.externalregistry.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import type { Cloudly } from 'ts/classes.cloudly.js';
|
||||||
|
import type { ExternalRegistryManager } from './classes.externalregistrymanager.js';
|
||||||
|
|
||||||
|
export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> {
|
||||||
|
// STATIC
|
||||||
|
public static async getRegistryById(registryIdArg: string) {
|
||||||
|
const externalRegistry = await this.getInstance({
|
||||||
|
id: registryIdArg,
|
||||||
|
});
|
||||||
|
return externalRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getRegistries() {
|
||||||
|
const externalRegistries = await this.getInstances({});
|
||||||
|
return externalRegistries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
|
||||||
|
const externalRegistry = new ExternalRegistry();
|
||||||
|
externalRegistry.id = await ExternalRegistry.getNewId();
|
||||||
|
Object.assign(externalRegistry, registryDataArg);
|
||||||
|
await externalRegistry.save();
|
||||||
|
return externalRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import { ExternalRegistry } from './classes.externalregistry.js';
|
||||||
|
|
||||||
|
export class ExternalRegistryManager {
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public CExternalRegistry = plugins.smartdata.setDefaultManagerForDoc(this, ExternalRegistry);
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
// lets set up a typedrouter
|
||||||
|
this.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>(
|
||||||
|
new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => {
|
||||||
|
const registry = await ExternalRegistry.getRegistryById(dataArg.id);
|
||||||
|
return {
|
||||||
|
registry: await registry.createSavableObject(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>(
|
||||||
|
new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => {
|
||||||
|
const registries = await ExternalRegistry.getRegistries();
|
||||||
|
return {
|
||||||
|
registries: await Promise.all(
|
||||||
|
registries.map((registry) => registry.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>(
|
||||||
|
new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => {
|
||||||
|
const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData);
|
||||||
|
return {
|
||||||
|
registry: await registry.createSavableObject(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
2
ts/manager.externalregistry/index.ts
Normal file
2
ts/manager.externalregistry/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './classes.externalregistrymanager.js';
|
||||||
|
export * from './classes.externalregistry.js';
|
@@ -1,12 +1,26 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { ImageManager } from './classes.imagemanager.js';
|
import type { ImageManager } from './classes.imagemanager.js';
|
||||||
|
|
||||||
@plugins.smartdata.Manager()
|
@plugins.smartdata.managed()
|
||||||
export class Image extends plugins.smartdata.SmartDataDbDoc<Image, plugins.servezoneInterfaces.data.IImage, ImageManager> {
|
export class Image extends plugins.smartdata.SmartDataDbDoc<
|
||||||
public static async create(imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>) {
|
Image,
|
||||||
|
plugins.servezoneInterfaces.data.IImage,
|
||||||
|
ImageManager
|
||||||
|
> {
|
||||||
|
public static async create(
|
||||||
|
imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>,
|
||||||
|
) {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.id = plugins.smartunique.uni('image');
|
image.id = await this.getNewId();
|
||||||
Object.assign(image.data, imageDataArg);
|
console.log(imageDataArg);
|
||||||
|
Object.assign(image, {
|
||||||
|
data: {
|
||||||
|
name: imageDataArg.name,
|
||||||
|
description: imageDataArg.description,
|
||||||
|
versions: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log((Image as any).saveableProperties);
|
||||||
await image.save();
|
await image.save();
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
@@ -18,4 +32,16 @@ export class Image extends plugins.smartdata.SmartDataDbDoc<Image, plugins.serve
|
|||||||
public data: plugins.servezoneInterfaces.data.IImage['data'];
|
public data: plugins.servezoneInterfaces.data.IImage['data'];
|
||||||
|
|
||||||
public async getVersions() {}
|
public async getVersions() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a storage path
|
||||||
|
* note: this is relative to the storage method defined by the imageManager
|
||||||
|
*/
|
||||||
|
public async getStoragePath(versionStringArg: string) {
|
||||||
|
return `${this.data.name}:${versionStringArg}`.replace('/', '__');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getWriteStream() {}
|
||||||
|
|
||||||
|
public async getReadStream() {}
|
||||||
}
|
}
|
@@ -1,97 +1,173 @@
|
|||||||
import type { Cloudly } from '../classes.cloudly.js';
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
|
||||||
import { Image } from './classes.image.js';
|
import { Image } from './classes.image.js';
|
||||||
|
|
||||||
export class ImageManager {
|
export class ImageManager {
|
||||||
cloudlyRef: Cloudly;
|
cloudlyRef: Cloudly;
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public smartbucketInstance: plugins.smartbucket.SmartBucket;
|
||||||
|
public imageDir: plugins.smartbucket.Directory;
|
||||||
|
public dockerImageStore: plugins.docker.DockerImageStore;
|
||||||
|
|
||||||
get db() {
|
get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
}
|
}
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
public CImage = plugins.smartdata.setDefaultManagerForDoc(this, Image);
|
public CImage = plugins.smartdata.setDefaultManagerForDoc(this, Image);
|
||||||
|
|
||||||
smartbucketInstance: plugins.smartbucket.SmartBucket;
|
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
|
|
||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
||||||
|
'createImage',
|
||||||
|
async (reqArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||||
|
const image = await this.CImage.create({
|
||||||
|
name: reqArg.name,
|
||||||
|
description: reqArg.description,
|
||||||
|
versions: [],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
image: await image.createSavableObject(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
||||||
|
new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||||
|
const image = await this.CImage.getInstance({
|
||||||
|
id: reqArg.imageId,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
image: await image.createSavableObject(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(
|
||||||
|
'deleteImage',
|
||||||
|
async (reqArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
|
||||||
|
const image = await this.CImage.getInstance({
|
||||||
|
id: reqArg.imageId,
|
||||||
|
});
|
||||||
|
await image.delete();
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
||||||
'getAllImages',
|
'getAllImages',
|
||||||
async (requestArg, toolsArg) => {
|
async (requestArg, toolsArg) => {
|
||||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], requestArg);
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg);
|
||||||
const images = await this.CImage.getInstances({});
|
const images = await this.CImage.getInstances({});
|
||||||
return {
|
return {
|
||||||
images: await Promise.all(
|
images: await Promise.all(
|
||||||
images.map((image) => {
|
images.map((image) => {
|
||||||
return image.createSavableObject();
|
return image.createSavableObject();
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PushImageVersion>(
|
||||||
'createImage',
|
'pushImageVersion',
|
||||||
async (reqArg) => {
|
async (reqArg, toolsArg) => {
|
||||||
const image = await this.CImage.create({
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
name: reqArg.name,
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
const refImage = await this.CImage.getInstance({
|
||||||
|
id: reqArg.imageId,
|
||||||
});
|
});
|
||||||
return {
|
if (!refImage) {
|
||||||
image: await image.createSavableObject(),
|
throw new plugins.typedrequest.TypedResponseError('Image not found');
|
||||||
};
|
|
||||||
}
|
}
|
||||||
)
|
const imageVersion = reqArg.versionString;
|
||||||
|
console.log(
|
||||||
|
`got request to push image version ${imageVersion} for image ${refImage.data.name}`,
|
||||||
);
|
);
|
||||||
|
const imagePushStream = reqArg.imageStream;
|
||||||
this.typedrouter.addTypedHandler(
|
(async () => {
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PushImage>(
|
const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream<
|
||||||
'pushImage',
|
Uint8Array,
|
||||||
async (reqArg) => {
|
Uint8Array
|
||||||
const pushStream = reqArg.imageStream;
|
>({
|
||||||
return {};
|
writeFunction: async (chunkArg, toolsArg) => {
|
||||||
}
|
console.log(chunkArg);
|
||||||
)
|
return chunkArg;
|
||||||
);
|
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImage>(
|
|
||||||
'pullImage',
|
|
||||||
async (reqArg) => {
|
|
||||||
const image = await this.CImage.getInstance({
|
|
||||||
data: {
|
|
||||||
name: reqArg.name,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const imageVersion = null;
|
imagePushStream.writeToWebstream(smartWebDuplex.writable);
|
||||||
|
await this.dockerImageStore.storeImage(
|
||||||
|
refImage.id,
|
||||||
|
plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable),
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImageVersion>(
|
||||||
|
'pullImageVersion',
|
||||||
|
async (reqArg) => {
|
||||||
|
const image = await this.CImage.getInstance({
|
||||||
|
id: reqArg.imageId,
|
||||||
|
});
|
||||||
|
const imageVersion = image.data.versions.find(
|
||||||
|
(version) => version.versionString === reqArg.versionString,
|
||||||
|
);
|
||||||
|
const readable = this.imageDir.fastGetStream(
|
||||||
|
{
|
||||||
|
path: await image.getStoragePath(reqArg.versionString),
|
||||||
|
},
|
||||||
|
'webstream',
|
||||||
|
);
|
||||||
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
|
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
|
||||||
return {
|
return {
|
||||||
imageStream: imageVirtualStream,
|
imageStream: imageVirtualStream,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
|
// lets setup s3
|
||||||
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
|
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
|
||||||
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
|
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
|
||||||
console.log(this.cloudlyRef.config.data.s3Descriptor);
|
console.log(this.cloudlyRef.config.data.s3Descriptor);
|
||||||
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
||||||
this.cloudlyRef.config.data.s3Descriptor
|
this.cloudlyRef.config.data.s3Descriptor,
|
||||||
);
|
);
|
||||||
const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test');
|
const bucket = await this.smartbucketInstance.getBucketByName(s3Descriptor.bucketName);
|
||||||
await bucket.fastPut({ path: 'test/test.txt', contents: 'hello' });
|
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
|
||||||
}
|
|
||||||
|
|
||||||
public async createImage(nameArg: string) {
|
this.imageDir = await bucket.getDirectoryFromPath({
|
||||||
const newImage = await this.CImage.create({
|
path: '/images',
|
||||||
name: nameArg,
|
});
|
||||||
|
|
||||||
|
// lets setup dockerstore
|
||||||
|
await plugins.smartfile.fs.ensureDir(paths.dockerImageStoreDir);
|
||||||
|
this.dockerImageStore = new plugins.docker.DockerImageStore({
|
||||||
|
localDirPath: paths.dockerImageStoreDir,
|
||||||
|
bucketDir: this.imageDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
secretGroups.push(
|
secretGroups.push(
|
||||||
await SecretGroup.getInstance({
|
await SecretGroup.getInstance({
|
||||||
id: secretGroupId,
|
id: secretGroupId,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return secretGroups;
|
return secretGroups;
|
||||||
@@ -59,4 +59,16 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
return returnObject;
|
return returnObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFlatKeyValueObject(environmentArg: string) {
|
||||||
|
if (!environmentArg) {
|
||||||
|
throw new Error('environment is required');
|
||||||
|
}
|
||||||
|
const secretGroups = await this.getSecretGroups();
|
||||||
|
const returnObject = {};
|
||||||
|
for (const secretGroup of secretGroups) {
|
||||||
|
returnObject[secretGroup.data.key] = secretGroup.data.environments[environmentArg].value;
|
||||||
|
}
|
||||||
|
return returnObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,99 +35,142 @@ export class CloudlySecretManager {
|
|||||||
this.typedrouter = new plugins.typedrequest.TypedRouter();
|
this.typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
// secretbundle routes
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundles>(
|
||||||
'adminGetConfigBundlesAndSecretGroups',
|
new plugins.typedrequest.TypedHandler(
|
||||||
async (dataArg) => {
|
'getSecretBundles',
|
||||||
dataArg.jwt
|
async (dataArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
|
dataArg.identity.jwt;
|
||||||
const secretBundles = await SecretBundle.getInstances({});
|
const secretBundles = await SecretBundle.getInstances({});
|
||||||
const secretGroups = await SecretGroup.getInstances({});
|
|
||||||
return {
|
return {
|
||||||
secretBundles: [
|
secretBundles: [
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
secretBundles.map((configBundle) => configBundle.createSavableObject())
|
secretBundles.map((configBundle) => configBundle.createSavableObject()),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_CreateSecretBundle>(
|
||||||
|
new plugins.typedrequest.TypedHandler('createSecretBundle', async (dataArg) => {
|
||||||
|
const secretBundle = new SecretBundle();
|
||||||
|
secretBundle.id = plugins.smartunique.shortId(8);
|
||||||
|
secretBundle.data = dataArg.secretBundle.data;
|
||||||
|
await secretBundle.save();
|
||||||
|
return {
|
||||||
|
resultSecretBundle: await secretBundle.createSavableObject(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_UpdateSecretBundle>(
|
||||||
|
new plugins.typedrequest.TypedHandler('updateSecretBundle', async (dataArg) => {
|
||||||
|
const secretBundle = await SecretBundle.getInstance({
|
||||||
|
id: dataArg.secretBundle.id,
|
||||||
|
});
|
||||||
|
secretBundle.data = dataArg.secretBundle.data;
|
||||||
|
await secretBundle.save();
|
||||||
|
return {
|
||||||
|
resultSecretBundle: await secretBundle.createSavableObject(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(
|
||||||
|
new plugins.typedrequest.TypedHandler('deleteSecretBundleById', async (dataArg) => {
|
||||||
|
const secretBundle = await SecretBundle.getInstance({
|
||||||
|
id: dataArg.secretBundleId,
|
||||||
|
});
|
||||||
|
await secretBundle.delete();
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// secretgroup routes
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretgroup.IReq_GetSecretGroups>(
|
||||||
|
new plugins.typedrequest.TypedHandler(
|
||||||
|
'getSecretGroups',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
|
dataArg.identity.jwt;
|
||||||
|
const secretGroups = await SecretGroup.getInstances({});
|
||||||
|
return {
|
||||||
secretGroups: [
|
secretGroups: [
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
secretGroups.map((secretGroup) => secretGroup.createSavableObject())
|
secretGroups.map((secretGroup) => secretGroup.createSavableObject()),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_CreateConfigBundlesAndSecretGroups>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretgroup.IReq_CreateSecretGroup>(
|
||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler('createSecretGroup', async (dataArg) => {
|
||||||
'adminCreateConfigBundlesAndSecretGroups',
|
|
||||||
async (dataArg) => {
|
|
||||||
for (const secretGroupObject of dataArg.secretGroups) {
|
|
||||||
const secretGroup = new SecretGroup();
|
const secretGroup = new SecretGroup();
|
||||||
secretGroup.id = plugins.smartunique.shortId(8);
|
secretGroup.id = plugins.smartunique.shortId(8);
|
||||||
secretGroup.data = secretGroupObject.data;
|
secretGroup.data = dataArg.secretGroup.data;
|
||||||
await secretGroup.save();
|
await secretGroup.save();
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
resultSecretGroup: await secretGroup.createSavableObject(),
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretgroup.IReq_UpdateSecretGroup>(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_DeleteConfigBundlesAndSecretGroups>(
|
new plugins.typedrequest.TypedHandler('updateSecretGroup', async (dataArg) => {
|
||||||
'adminDeleteConfigBundlesAndSecretGroups',
|
|
||||||
async (dataArg) => {
|
|
||||||
for (const secretGroupId of dataArg.secretGroupIds) {
|
|
||||||
const secretGroup = await SecretGroup.getInstance({
|
const secretGroup = await SecretGroup.getInstance({
|
||||||
id: secretGroupId,
|
id: dataArg.secretGroup.id,
|
||||||
|
});
|
||||||
|
secretGroup.data = dataArg.secretGroup.data;
|
||||||
|
await secretGroup.save();
|
||||||
|
return {
|
||||||
|
resultSecretGroup: await secretGroup.createSavableObject(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(
|
||||||
|
new plugins.typedrequest.TypedHandler('deleteSecretGroupById', async (dataArg) => {
|
||||||
|
const secretGroup = await SecretGroup.getInstance({
|
||||||
|
id: dataArg.secretGroupId,
|
||||||
});
|
});
|
||||||
await secretGroup.delete();
|
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 {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// lets add typedrouter routes for accessing the configvailt from apps
|
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_GetEnvBundle>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetFlatKeyValueObject>(
|
||||||
'getEnvBundle',
|
'getFlatKeyValueObject',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const wantedBundle = await SecretBundle.getInstance({
|
const wantedBundle = await SecretBundle.getInstance({
|
||||||
data: {
|
data: {
|
||||||
authorizations: {
|
authorizations: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
$elemMatch: {
|
$elemMatch: {
|
||||||
secretAccessKey: dataArg.authorization,
|
secretAccessKey: dataArg.secretBundleAuthorization.secretAccessKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
|
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
|
||||||
dataArg.authorization
|
dataArg.secretBundleAuthorization.secretAccessKey,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
envBundle: {
|
flatKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
|
||||||
configKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
|
authorization.environment,
|
||||||
authorization.environment
|
|
||||||
),
|
),
|
||||||
environment: authorization.environment,
|
|
||||||
timeSensitive: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
90
ts/manager.server/classes.curlfresh.ts
Normal file
90
ts/manager.server/classes.curlfresh.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { logger } from '../logger.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { CloudlyServerManager } from './classes.servermanager.js';
|
||||||
|
|
||||||
|
export class CurlFresh {
|
||||||
|
public optionsArg = {
|
||||||
|
npmRegistry: 'https://registry.npmjs.org',
|
||||||
|
};
|
||||||
|
public scripts = {
|
||||||
|
'setup.sh': `#!/bin/bash
|
||||||
|
|
||||||
|
# lets update the system and install curl
|
||||||
|
# might be installed already, but entrypoint could have been wget
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --force-yes curl
|
||||||
|
|
||||||
|
# Basic updating of the software lists
|
||||||
|
echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
|
apt-get update
|
||||||
|
apt-get upgrade -y --force-yes
|
||||||
|
apt-get install -y --force-yes fail2ban curl git
|
||||||
|
curl -sL https://deb.nodesource.com/setup_18.x | bash
|
||||||
|
|
||||||
|
# Install docker
|
||||||
|
curl -sSL https://get.docker.com/ | sh
|
||||||
|
|
||||||
|
# Install default nodejs to run nodejs tools
|
||||||
|
apt-get install -y nodejs zsh
|
||||||
|
zsh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
|
||||||
|
npm config set unsafe-perm true
|
||||||
|
|
||||||
|
# lets install pnpm
|
||||||
|
curl -fsSL https://get.pnpm.io/install.sh | sh -
|
||||||
|
|
||||||
|
# lets make sure we use the correct npm registry
|
||||||
|
bash -c "npm config set registry ${this.optionsArg.npmRegistry}"
|
||||||
|
|
||||||
|
# lets install spark
|
||||||
|
bash -c "pnpm install -g @serve.zone/spark"
|
||||||
|
|
||||||
|
# lets install the spark daemon
|
||||||
|
bash -c "spark installdaemon"
|
||||||
|
|
||||||
|
# TODO: start spark with jump code
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
public serverManagerRef: CloudlyServerManager;
|
||||||
|
public curlFreshRoute: plugins.typedserver.servertools.Route;
|
||||||
|
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
||||||
|
logger.log('info', 'curlfresh handler called. a server might be coming online soon :)');
|
||||||
|
const scriptname = req.params.scriptname;
|
||||||
|
switch (scriptname) {
|
||||||
|
case 'setup.sh':
|
||||||
|
logger.log('info', 'sending setup.sh');
|
||||||
|
res.type('application/x-sh');
|
||||||
|
res.send(this.scripts['setup.sh']);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
res.send('no script found');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(serverManagerRefArg: CloudlyServerManager) {
|
||||||
|
this.serverManagerRef = serverManagerRefArg;
|
||||||
|
}
|
||||||
|
public async getServerUserData(): Promise<string> {
|
||||||
|
const sslMode =
|
||||||
|
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
||||||
|
let protocol: 'http' | 'https';
|
||||||
|
if (sslMode === 'none') {
|
||||||
|
protocol = 'http';
|
||||||
|
} else {
|
||||||
|
protocol = 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain =
|
||||||
|
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl');
|
||||||
|
const port =
|
||||||
|
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
|
||||||
|
|
||||||
|
const serverUserData = `#cloud-config
|
||||||
|
runcmd:
|
||||||
|
- curl -o- ${protocol}://${domain}:${port}/curlfresh/setup.sh | sh
|
||||||
|
`;
|
||||||
|
console.log(serverUserData);
|
||||||
|
return serverUserData;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,10 +4,13 @@ import * as plugins from '../plugins.js';
|
|||||||
* cluster defines a swarmkit cluster
|
* cluster defines a swarmkit cluster
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.Manager()
|
@plugins.smartdata.Manager()
|
||||||
export class Server extends plugins.smartdata.SmartDataDbDoc<Server, plugins.servezoneInterfaces.data.IServer> {
|
export class Server extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Server,
|
||||||
|
plugins.servezoneInterfaces.data.IServer
|
||||||
|
> {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static async createFromHetznerServer(
|
public static async createFromHetznerServer(
|
||||||
hetznerServerArg: plugins.hetznercloud.HetznerServer
|
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
||||||
) {
|
) {
|
||||||
const newServer = new Server();
|
const newServer = new Server();
|
||||||
newServer.id = plugins.smartunique.shortId(8);
|
newServer.id = plugins.smartunique.shortId(8);
|
||||||
@@ -16,7 +19,7 @@ export class Server extends plugins.smartdata.SmartDataDbDoc<Server, plugins.ser
|
|||||||
requiredDebianPackages: [],
|
requiredDebianPackages: [],
|
||||||
sshKeys: [],
|
sshKeys: [],
|
||||||
type: 'hetzner',
|
type: 'hetzner',
|
||||||
}
|
};
|
||||||
Object.assign(newServer, { data });
|
Object.assign(newServer, { data });
|
||||||
await newServer.save();
|
await newServer.save();
|
||||||
return newServer;
|
return newServer;
|
@@ -1,11 +1,13 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { Cloudly } from '../classes.cloudly.js';
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
import { Cluster } from '../manager.cluster/cluster.js';
|
import { Cluster } from '../manager.cluster/classes.cluster.js';
|
||||||
import { Server } from './server.js';
|
import { Server } from './classes.server.js';
|
||||||
|
import { CurlFresh } from './classes.curlfresh.js';
|
||||||
|
|
||||||
export class CloudlyServerManager {
|
export class CloudlyServerManager {
|
||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public curlfreshInstance = new CurlFresh(this);
|
||||||
|
|
||||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
||||||
|
|
||||||
@@ -27,17 +29,19 @@ export class CloudlyServerManager {
|
|||||||
const serverId = requestData.serverId;
|
const serverId = requestData.serverId;
|
||||||
const server = await this.CServer.getInstance({
|
const server = await this.CServer.getInstance({
|
||||||
id: serverId,
|
id: serverId,
|
||||||
})
|
});
|
||||||
return {
|
return {
|
||||||
configData: await server.createSavableObject(),
|
configData: await server.createSavableObject(),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(this.cloudlyRef.config.data.hetznerToken);
|
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(
|
||||||
|
this.cloudlyRef.config.data.hetznerToken,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {}
|
public async stop() {}
|
||||||
@@ -63,16 +67,19 @@ export class CloudlyServerManager {
|
|||||||
labels: {
|
labels: {
|
||||||
clusterId: cluster.id,
|
clusterId: cluster.id,
|
||||||
priority: '1',
|
priority: '1',
|
||||||
}
|
},
|
||||||
|
userData: await this.curlfreshInstance.getServerUserData(),
|
||||||
});
|
});
|
||||||
const newServer = await Server.createFromHetznerServer(server);
|
const newServer = await Server.createFromHetznerServer(server);
|
||||||
console.log(`cluster created new server for cluster ${cluster.id}`);
|
console.log(`cluster created new server for cluster ${cluster.id}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`cluster ${cluster.id} already has servers. Making sure that they actually exist in the real world...`);
|
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
|
// if there is a server, make sure that it exists
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const hetznerServer = await this.hetznerAccount.getServersByLabel({
|
const hetznerServer = await this.hetznerAccount.getServersByLabel({
|
||||||
'clusterId': cluster.id
|
clusterId: cluster.id,
|
||||||
});
|
});
|
||||||
if (!hetznerServer) {
|
if (!hetznerServer) {
|
||||||
console.log(`server ${server.id} does not exist in the real world. Creating it now...`);
|
console.log(`server ${server.id} does not exist in the real world. Creating it now...`);
|
||||||
@@ -83,7 +90,7 @@ export class CloudlyServerManager {
|
|||||||
labels: {
|
labels: {
|
||||||
clusterId: cluster.id,
|
clusterId: cluster.id,
|
||||||
priority: '1',
|
priority: '1',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const newServer = await Server.createFromHetznerServer(hetznerServer);
|
const newServer = await Server.createFromHetznerServer(hetznerServer);
|
||||||
}
|
}
|
||||||
@@ -96,7 +103,7 @@ export class CloudlyServerManager {
|
|||||||
const results = await this.CServer.getInstances({
|
const results = await this.CServer.getInstances({
|
||||||
data: {
|
data: {
|
||||||
assignedClusterId: clusterArg.id,
|
assignedClusterId: clusterArg.id,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { ServiceManager } from './classes.servicemanager.js';
|
||||||
|
|
||||||
|
export class Service extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Service,
|
||||||
|
plugins.servezoneInterfaces.data.IService,
|
||||||
|
ServiceManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getServiceById(serviceIdArg: string) {
|
||||||
|
const service = await this.getInstance({
|
||||||
|
id: serviceIdArg,
|
||||||
|
});
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getServices() {
|
||||||
|
const services = await this.getInstances({});
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createService(serviceDataArg: Partial<plugins.servezoneInterfaces.data.IService['data']>) {
|
||||||
|
const service = new Service();
|
||||||
|
service.id = await Service.getNewId();
|
||||||
|
Object.assign(service, serviceDataArg);
|
||||||
|
await service.save();
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IService['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a service runs in a specific environment
|
||||||
|
* so -> this method returns the secret bundles as a flat object accordingly.
|
||||||
|
* in other words, it resolves secret groups for the relevant environment
|
||||||
|
* @param environmentArg
|
||||||
|
*/
|
||||||
|
public async getSecretBundlesAsFlatObject(environmentArg: string = 'production') {
|
||||||
|
const secreBundleIds = this.data.additionalSecretBundleIds || [];
|
||||||
|
secreBundleIds.push(this.data.secretBundleId); // put this last, so it overwrites any other secret bundles.
|
||||||
|
let finalFlatObject = {};
|
||||||
|
for (const secretBundleId of secreBundleIds) {
|
||||||
|
const secretBundle = await SecretBundle.getInstance({
|
||||||
|
id: secretBundleId,
|
||||||
|
});
|
||||||
|
const flatObject = await secretBundle.getFlatKeyValueObject(environmentArg);
|
||||||
|
Object.assign(finalFlatObject, flatObject);
|
||||||
|
}
|
||||||
|
return finalFlatObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,100 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Service } from './classes.service.js';
|
||||||
|
|
||||||
|
export class ServiceManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CService = plugins.smartdata.setDefaultManagerForDoc(this, Service);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
||||||
|
'getServices',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const services = await this.CService.getInstances({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
services: await Promise.all(
|
||||||
|
services.map((service) => {
|
||||||
|
return service.createSavableObject();
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject>(
|
||||||
|
'getServiceSecretBundlesAsFlatObject',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
const flatKeyValueObject = await service.getSecretBundlesAsFlatObject(dataArg.environment);
|
||||||
|
return {
|
||||||
|
flatKeyValueObject: flatKeyValueObject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
||||||
|
'createService',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.createService(dataArg.serviceData);
|
||||||
|
return {
|
||||||
|
service: await service.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
||||||
|
'updateService',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
service.data = {
|
||||||
|
...service.data,
|
||||||
|
...dataArg.serviceData,
|
||||||
|
};
|
||||||
|
await service.save();
|
||||||
|
return {
|
||||||
|
service: await service.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
||||||
|
'deleteServiceById',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
await service.delete();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -16,7 +16,7 @@ export class ExternalApiManager {
|
|||||||
return {
|
return {
|
||||||
networkNodes,
|
networkNodes,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
|
export const packageDir = plugins.path.join(
|
||||||
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
|
'../',
|
||||||
|
);
|
||||||
export const nogitDir = plugins.path.join(packageDir, '.nogit/');
|
export const nogitDir = plugins.path.join(packageDir, '.nogit/');
|
||||||
|
export const dockerImageStoreDir = plugins.path.join(nogitDir, './dockerimagestore/');
|
||||||
export const distServeDir = plugins.path.join(packageDir, './dist_serve');
|
export const distServeDir = plugins.path.join(packageDir, './dist_serve');
|
||||||
|
@@ -9,13 +9,13 @@ import * as typedsocket from '@api.global/typedsocket';
|
|||||||
|
|
||||||
export { typedrequest, typedsocket };
|
export { typedrequest, typedsocket };
|
||||||
|
|
||||||
// @mojoio scope
|
// @apiclient.xyz scope
|
||||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||||
import * as digitalocean from '@apiclient.xyz/digitalocean';
|
import * as docker from '@apiclient.xyz/docker';
|
||||||
import * as hetznercloud from '@apiclient.xyz/hetznercloud';
|
import * as hetznercloud from '@apiclient.xyz/hetznercloud';
|
||||||
import * as slack from '@apiclient.xyz/slack';
|
import * as slack from '@apiclient.xyz/slack';
|
||||||
|
|
||||||
export { cloudflare, digitalocean, hetznercloud, slack };
|
export { cloudflare, docker, hetznercloud, slack };
|
||||||
|
|
||||||
// @tsclass scope
|
// @tsclass scope
|
||||||
import * as tsclass from '@tsclass/tsclass';
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
@@ -29,9 +29,11 @@ import * as qenv from '@push.rocks/qenv';
|
|||||||
import * as smartacme from '@push.rocks/smartacme';
|
import * as smartacme from '@push.rocks/smartacme';
|
||||||
import * as smartbucket from '@push.rocks/smartbucket';
|
import * as smartbucket from '@push.rocks/smartbucket';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
|
import * as smartclickhouse from '@push.rocks/smartclickhouse';
|
||||||
import * as smartdata from '@push.rocks/smartdata';
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartexit from '@push.rocks/smartexit';
|
import * as smartexit from '@push.rocks/smartexit';
|
||||||
|
import * as smartexpect from '@push.rocks/smartexpect';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartguard from '@push.rocks/smartguard';
|
import * as smartguard from '@push.rocks/smartguard';
|
||||||
import * as smartjson from '@push.rocks/smartjson';
|
import * as smartjson from '@push.rocks/smartjson';
|
||||||
@@ -41,6 +43,7 @@ import * as smartpath from '@push.rocks/smartpath';
|
|||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartrequest from '@push.rocks/smartrequest';
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
import * as smartssh from '@push.rocks/smartssh';
|
import * as smartssh from '@push.rocks/smartssh';
|
||||||
|
import * as smartstream from '@push.rocks/smartstream';
|
||||||
import * as smartstring from '@push.rocks/smartstring';
|
import * as smartstring from '@push.rocks/smartstring';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
@@ -53,8 +56,10 @@ export {
|
|||||||
smartacme,
|
smartacme,
|
||||||
smartbucket,
|
smartbucket,
|
||||||
smartcli,
|
smartcli,
|
||||||
|
smartclickhouse,
|
||||||
smartdata,
|
smartdata,
|
||||||
smartexit,
|
smartexit,
|
||||||
|
smartexpect,
|
||||||
smartdelay,
|
smartdelay,
|
||||||
smartfile,
|
smartfile,
|
||||||
smartguard,
|
smartguard,
|
||||||
@@ -65,6 +70,7 @@ export {
|
|||||||
smartpromise,
|
smartpromise,
|
||||||
smartrequest,
|
smartrequest,
|
||||||
smartssh,
|
smartssh,
|
||||||
|
smartstream,
|
||||||
smartstring,
|
smartstring,
|
||||||
smartunique,
|
smartunique,
|
||||||
taskbuffer,
|
taskbuffer,
|
||||||
|
240
ts_apiclient/classes.cloudlyapiclient.ts
Normal file
240
ts_apiclient/classes.cloudlyapiclient.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export type TClientType = 'api' | 'ci' | 'coreflow' | 'cli' | 'serverconfig';
|
||||||
|
|
||||||
|
import { Image } from './classes.image.js';
|
||||||
|
import { Service } from './classes.service.js';
|
||||||
|
import { Cluster } from './classes.cluster.js';
|
||||||
|
import { SecretBundle } from './classes.secretbundle.js';
|
||||||
|
import { SecretGroup } from './classes.secretgroup.js';
|
||||||
|
import { ExternalRegistry } from './classes.externalregistry.js';
|
||||||
|
|
||||||
|
export class CloudlyApiClient {
|
||||||
|
private cloudlyUrl: string;
|
||||||
|
private registerAs: string;
|
||||||
|
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public typedsocketClient: plugins.typedsocket.TypedSocket;
|
||||||
|
|
||||||
|
// Subjects
|
||||||
|
public configUpdateSubject = new plugins.smartrx.rxjs.Subject<
|
||||||
|
plugins.servezoneInterfaces.requests.config.IRequest_Cloudly_Coreflow_PushClusterConfig['request']
|
||||||
|
>();
|
||||||
|
|
||||||
|
public serverActionSubject = new plugins.smartrx.rxjs.Subject<
|
||||||
|
plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction['request']
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(optionsArg: {
|
||||||
|
registerAs: TClientType;
|
||||||
|
cloudlyUrl?: string;
|
||||||
|
}) {
|
||||||
|
this.registerAs = optionsArg.registerAs;
|
||||||
|
this.cloudlyUrl =
|
||||||
|
optionsArg?.cloudlyUrl || process.env.CLOUDLY_URL || 'https://cloudly.layer.io:443';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`creating LoleCloudlyClient: registering as ${this.registerAs} and target url ${this.cloudlyUrl}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Cloudly_Coreflow_PushClusterConfig>(
|
||||||
|
new plugins.typedrequest.TypedHandler('pushClusterConfig', async (dataArg) => {
|
||||||
|
this.configUpdateSubject.next(dataArg);
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction>(
|
||||||
|
new plugins.typedrequest.TypedHandler('triggerServerAction', async (dataArg) => {
|
||||||
|
this.serverActionSubject.next(dataArg);
|
||||||
|
return {
|
||||||
|
actionConfirmed: true,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
||||||
|
this.typedrouter,
|
||||||
|
this.cloudlyUrl
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`CloudlyClient connected to cloudly at ${this.cloudlyUrl}. Remember to get an identity.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
await this.typedsocketClient.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
|
public async getIdentityByToken(
|
||||||
|
token: string,
|
||||||
|
optionsArg?: {
|
||||||
|
tagConnection?: boolean;
|
||||||
|
statefullIdentity?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<plugins.servezoneInterfaces.data.IIdentity> {
|
||||||
|
optionsArg = Object.assign({}, {
|
||||||
|
tagConnection: false,
|
||||||
|
statefullIdentity: true,
|
||||||
|
}, optionsArg);
|
||||||
|
|
||||||
|
const identityRequest =
|
||||||
|
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByToken>(
|
||||||
|
'getIdentityByToken'
|
||||||
|
);
|
||||||
|
console.log(`trying to get identity from cloudly with supplied jumpCodeArg: ${token}`);
|
||||||
|
const response = await identityRequest.fire({
|
||||||
|
token: token,
|
||||||
|
});
|
||||||
|
console.log('got identity response');
|
||||||
|
const identity = response.identity;
|
||||||
|
|
||||||
|
if (optionsArg.tagConnection) {
|
||||||
|
this.typedsocketClient.addTag('identity', identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsArg.statefullIdentity) {
|
||||||
|
this.identity = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* will use statefull identity by default
|
||||||
|
*/
|
||||||
|
public async getClusterConfigFromCloudlyByIdentity(
|
||||||
|
identityArg: plugins.servezoneInterfaces.data.IIdentity = this.identity
|
||||||
|
): Promise<plugins.servezoneInterfaces.data.ICluster> {
|
||||||
|
const clusterConfigRequest =
|
||||||
|
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig>(
|
||||||
|
'getClusterConfig'
|
||||||
|
);
|
||||||
|
const response = await clusterConfigRequest.fire({
|
||||||
|
identity: identityArg,
|
||||||
|
});
|
||||||
|
return response.configData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* will use statefull identity by default
|
||||||
|
*/
|
||||||
|
public async getServerConfigFromCloudlyByIdentity(
|
||||||
|
identityArg: plugins.servezoneInterfaces.data.IIdentity = this.identity
|
||||||
|
): Promise<plugins.servezoneInterfaces.data.IServer> {
|
||||||
|
const serverConfigRequest =
|
||||||
|
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
|
||||||
|
'getServerConfig'
|
||||||
|
);
|
||||||
|
const response = await serverConfigRequest.fire({
|
||||||
|
identity: identityArg,
|
||||||
|
serverId: '', // TODO: get server id here
|
||||||
|
});
|
||||||
|
return response.configData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets a certificate for a domain used by a service
|
||||||
|
*/
|
||||||
|
public async getCertificateForDomain(optionsArg: {
|
||||||
|
domainName: string;
|
||||||
|
type: plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetCertificateForDomain['request']['type'];
|
||||||
|
identity?: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
|
}): Promise<plugins.tsclass.network.ICert> {
|
||||||
|
optionsArg.identity = optionsArg.identity || this.identity;
|
||||||
|
if (!optionsArg.identity) {
|
||||||
|
throw new Error('identity is required. Either provide one or login first.');
|
||||||
|
}
|
||||||
|
const typedCertificateRequest =
|
||||||
|
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetCertificateForDomain>(
|
||||||
|
'getCertificateForDomain'
|
||||||
|
);
|
||||||
|
const typedResponse = await typedCertificateRequest.fire({
|
||||||
|
identity: this.identity, // do proper auth here
|
||||||
|
domainName: optionsArg.domainName,
|
||||||
|
type: optionsArg.type,
|
||||||
|
});
|
||||||
|
return typedResponse.certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public externalRegistry = {
|
||||||
|
// ExternalRegistry
|
||||||
|
getRegistryById: async (registryNameArg: string) => {
|
||||||
|
return ExternalRegistry.getExternalRegistryById(this, registryNameArg);
|
||||||
|
},
|
||||||
|
getRegistries: async () => {
|
||||||
|
return ExternalRegistry.getExternalRegistries(this);
|
||||||
|
},
|
||||||
|
createRegistry: async (optionsArg: Parameters<typeof ExternalRegistry.createExternalRegistry>[1]) => {
|
||||||
|
return ExternalRegistry.createExternalRegistry(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public image = {
|
||||||
|
// Images
|
||||||
|
getImageById: async (imageIdArg: string) => {
|
||||||
|
return Image.getImageById(this, imageIdArg);
|
||||||
|
},
|
||||||
|
getImages: async () => {
|
||||||
|
return Image.getImages(this);
|
||||||
|
},
|
||||||
|
createImage: async (optionsArg: Parameters<typeof Image.createImage>[1]) => {
|
||||||
|
return Image.createImage(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public services = {
|
||||||
|
// Services
|
||||||
|
getServiceById: async (serviceIdArg: string) => {
|
||||||
|
return Service.getServiceById(this, serviceIdArg);
|
||||||
|
},
|
||||||
|
getServices: async () => {
|
||||||
|
return Service.getServices(this);
|
||||||
|
},
|
||||||
|
createService: async (optionsArg: Parameters<typeof Service.createService>[1]) => {
|
||||||
|
return Service.createService(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cluster = {
|
||||||
|
// Clusters
|
||||||
|
getClusterById: async (clusterIdArg: string) => {
|
||||||
|
return Cluster.getClusterById(this, clusterIdArg);
|
||||||
|
},
|
||||||
|
getClusters: async () => {
|
||||||
|
return Cluster.getClusters(this);
|
||||||
|
},
|
||||||
|
createCluster: async (optionsArg: Parameters<typeof Cluster.createCluster>[1]) => {
|
||||||
|
return Cluster.createCluster(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public secretbundle = {
|
||||||
|
// SecretBundles
|
||||||
|
getSecretBundleById: async (secretBundleIdArg: string) => {
|
||||||
|
return SecretBundle.getSecretBundleById(this, secretBundleIdArg);
|
||||||
|
},
|
||||||
|
getSecretBundles: async () => {
|
||||||
|
return SecretBundle.getSecretBundles(this);
|
||||||
|
},
|
||||||
|
createSecretBundle: async (optionsArg: Parameters<typeof SecretBundle.createSecretBundle>[1]) => {
|
||||||
|
return SecretBundle.createSecretBundle(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public secretgroup = {
|
||||||
|
// SecretGroups
|
||||||
|
getSecretGroupById: async (secretGroupIdArg: string) => {
|
||||||
|
return SecretGroup.getSecretGroupById(this, secretGroupIdArg);
|
||||||
|
},
|
||||||
|
getSecretGroups: async () => {
|
||||||
|
return SecretGroup.getSecretGroups(this);
|
||||||
|
},
|
||||||
|
createSecretGroup: async (optionsArg: Parameters<typeof SecretGroup.createSecretGroup>[1]) => {
|
||||||
|
return SecretGroup.createSecretGroup(this, optionsArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
|
|
||||||
export type TClientType = 'coreflow' | 'cli' | 'serverconfig';
|
|
||||||
|
|
||||||
export class CloudlyClient {
|
|
||||||
private cloudlyUrl: string;
|
|
||||||
private registerAs: string;
|
|
||||||
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
public typedsocketClient: plugins.typedsocket.TypedSocket;
|
|
||||||
|
|
||||||
// Subjects
|
|
||||||
public configUpdateSubject = new plugins.smartrx.rxjs.Subject<
|
|
||||||
plugins.servezoneInterfaces.requests.config.IRequest_Cloudly_Coreflow_PushClusterConfig['request']
|
|
||||||
>();
|
|
||||||
|
|
||||||
public serverActionSubject = new plugins.smartrx.rxjs.Subject<
|
|
||||||
plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction['request']
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(registerAsArg: TClientType) {
|
|
||||||
this.cloudlyUrl = process.env.CLOUDLY_URL || 'https://cloudly.layer.io:443';
|
|
||||||
this.registerAs = registerAsArg;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`creating LoleCloudlyClient: registering as ${this.registerAs} and target url ${this.cloudlyUrl}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Cloudly_Coreflow_PushClusterConfig>(
|
|
||||||
new plugins.typedrequest.TypedHandler('pushClusterConfig', async (dataArg) => {
|
|
||||||
this.configUpdateSubject.next(dataArg);
|
|
||||||
return {};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Cloudly_Coreflow_PushClusterConfig>(
|
|
||||||
new plugins.typedrequest.TypedHandler('pushClusterConfig', async (dataArg) => {
|
|
||||||
this.configUpdateSubject.next(dataArg);
|
|
||||||
return {};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction>(
|
|
||||||
new plugins.typedrequest.TypedHandler('triggerServerAction', async (dataArg) => {
|
|
||||||
this.serverActionSubject.next(dataArg);
|
|
||||||
return {
|
|
||||||
actionConfirmed: true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
|
||||||
this.typedrouter,
|
|
||||||
this.cloudlyUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop() {
|
|
||||||
await this.typedsocketClient.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentityByJumpCode(
|
|
||||||
jumpCodeArg: string,
|
|
||||||
tagConnection = false
|
|
||||||
): Promise<plugins.servezoneInterfaces.data.IClusterIdentifier> {
|
|
||||||
const identityRequest =
|
|
||||||
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>(
|
|
||||||
'getIdentityByJumpCode'
|
|
||||||
);
|
|
||||||
console.log(`trying to get identity from cloudly with supplied jumpCodeArg: ${jumpCodeArg}`);
|
|
||||||
const response = await identityRequest.fire({
|
|
||||||
jumpCode: jumpCodeArg,
|
|
||||||
});
|
|
||||||
console.log('got identity response');
|
|
||||||
const identity = response.clusterIdentifier;
|
|
||||||
|
|
||||||
if (tagConnection) {
|
|
||||||
this.typedsocketClient.addTag('identity', identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getClusterConfigFromCloudlyByIdentity(
|
|
||||||
identityArg: plugins.servezoneInterfaces.data.IClusterIdentifier
|
|
||||||
): Promise<plugins.servezoneInterfaces.data.ICluster> {
|
|
||||||
const clusterConfigRequest =
|
|
||||||
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetClusterConfig>(
|
|
||||||
'getClusterConfig'
|
|
||||||
);
|
|
||||||
const response = await clusterConfigRequest.fire({
|
|
||||||
jwt: '', // TODO: do proper auth here
|
|
||||||
clusterIdentifier: identityArg,
|
|
||||||
});
|
|
||||||
return response.configData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getServerConfigFromCloudlyByIdentity(
|
|
||||||
identityArg: plugins.servezoneInterfaces.data.IClusterIdentifier
|
|
||||||
): Promise<plugins.servezoneInterfaces.data.IServer> {
|
|
||||||
const serverConfigRequest =
|
|
||||||
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
|
|
||||||
'getServerConfig'
|
|
||||||
);
|
|
||||||
const response = await serverConfigRequest.fire({
|
|
||||||
jwt: '', // TODO: do proper auth here
|
|
||||||
serverId: '' // TODO: get server id here
|
|
||||||
});
|
|
||||||
return response.configData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets a certificate for a domain used by a service
|
|
||||||
* @param serviceNameArg
|
|
||||||
* @param domainNameArg
|
|
||||||
*/
|
|
||||||
public async getCertificateForDomainOverHttps(domainNameArg: string): Promise<plugins.tsclass.network.ICert> {
|
|
||||||
const typedCertificateRequest =
|
|
||||||
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetSslCertificate>(
|
|
||||||
'getSslCertificate'
|
|
||||||
);
|
|
||||||
const typedResponse = await typedCertificateRequest.fire({
|
|
||||||
authToken: '', // do proper auth here
|
|
||||||
requiredCertName: domainNameArg,
|
|
||||||
});
|
|
||||||
return typedResponse.certificate;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,84 @@
|
|||||||
|
import { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export class Cluster {
|
export class Cluster implements plugins.servezoneInterfaces.data.ICluster {
|
||||||
public getServers() {}
|
// STATIC
|
||||||
|
public static async getClusterById(cloudlyClientRef: CloudlyApiClient, clusterIdArg: string) {
|
||||||
|
const getClusterByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusterById>(
|
||||||
|
'getClusterById'
|
||||||
|
);
|
||||||
|
const response = await getClusterByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
clusterId: clusterIdArg,
|
||||||
|
});
|
||||||
|
const newCluster = new Cluster(cloudlyClientRef);
|
||||||
|
Object.assign(newCluster, response.cluster);
|
||||||
|
return newCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getClusters(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getClustersTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
||||||
|
'getClusters'
|
||||||
|
);
|
||||||
|
const response = await getClustersTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const clusterConfigs: Cluster[] = [];
|
||||||
|
for (const clusterConfig of response.clusters) {
|
||||||
|
const newCluster = new Cluster(cloudlyClientRef);
|
||||||
|
Object.assign(newCluster, clusterConfig);
|
||||||
|
clusterConfigs.push(newCluster);
|
||||||
|
}
|
||||||
|
return clusterConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createCluster(cloudlyClientRef: CloudlyApiClient, clusterNameArg: string) {
|
||||||
|
const createClusterTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
||||||
|
'createCluster'
|
||||||
|
);
|
||||||
|
const response = await createClusterTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
clusterName: clusterNameArg,
|
||||||
|
});
|
||||||
|
const newCluster = new Cluster(cloudlyClientRef);
|
||||||
|
Object.assign(newCluster, response.cluster);
|
||||||
|
return newCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public id: string;
|
||||||
|
public data: plugins.servezoneInterfaces.data.ICluster['data'];
|
||||||
|
public cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async update() {
|
||||||
|
const updateClusterTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_UpdateCluster>(
|
||||||
|
'updateCluster'
|
||||||
|
);
|
||||||
|
const response = await updateClusterTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
clusterData: this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultClusterData = response.resultCluster.data;
|
||||||
|
plugins.smartexpect.expect(resultClusterData).toEqual(this.data);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(cloudlyClientRef: CloudlyApiClient, clusterIdArg: string) {
|
||||||
|
const deleteClusterTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
|
||||||
|
'deleteClusterById'
|
||||||
|
);
|
||||||
|
const response = await deleteClusterTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
clusterId: this.id,
|
||||||
|
});
|
||||||
|
plugins.smartexpect.expect(response.ok).toBeTrue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
83
ts_apiclient/classes.externalregistry.ts
Normal file
83
ts_apiclient/classes.externalregistry.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import type { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
|
|
||||||
|
export class ExternalRegistry implements plugins.servezoneInterfaces.data.IExternalRegistry {
|
||||||
|
// STATIC
|
||||||
|
public static async getExternalRegistryById(cloudlyClientRef: CloudlyApiClient, registryNameArg: string) {
|
||||||
|
const getRegistryByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>(
|
||||||
|
'getExternalRegistryById'
|
||||||
|
);
|
||||||
|
const response = await getRegistryByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
id: registryNameArg,
|
||||||
|
});
|
||||||
|
const newRegistry = new ExternalRegistry(cloudlyClientRef);
|
||||||
|
Object.assign(newRegistry, response.registry);
|
||||||
|
return newRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getExternalRegistries(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getRegistriesTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>(
|
||||||
|
'getExternalRegistries'
|
||||||
|
);
|
||||||
|
const response = await getRegistriesTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const registryConfigs: ExternalRegistry[] = [];
|
||||||
|
for (const registryConfig of response.registries) {
|
||||||
|
const newRegistry = new ExternalRegistry(cloudlyClientRef);
|
||||||
|
Object.assign(newRegistry, registryConfig);
|
||||||
|
registryConfigs.push(newRegistry);
|
||||||
|
}
|
||||||
|
return registryConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createExternalRegistry(cloudlyClientRef: CloudlyApiClient, registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
|
||||||
|
const createRegistryTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>(
|
||||||
|
'createExternalRegistry'
|
||||||
|
);
|
||||||
|
const response = await createRegistryTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
registryData: registryDataArg as plugins.servezoneInterfaces.data.IExternalRegistry['data'],
|
||||||
|
});
|
||||||
|
const newRegistry = new ExternalRegistry(cloudlyClientRef);
|
||||||
|
Object.assign(newRegistry, response.registry);
|
||||||
|
return newRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public id: string;
|
||||||
|
public data: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
||||||
|
public cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update() {
|
||||||
|
const updateRegistryTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(
|
||||||
|
'updateExternalRegistry'
|
||||||
|
);
|
||||||
|
const response = await updateRegistryTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
registryData: this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultRegistryData = response.resultRegistry.data;
|
||||||
|
plugins.smartexpect.expect(resultRegistryData).toEqual(this.data);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(cloudlyClientRef: CloudlyApiClient, registryIdArg: string) {
|
||||||
|
const deleteRegistryTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(
|
||||||
|
'deleteExternalRegistryById'
|
||||||
|
);
|
||||||
|
const response = await deleteRegistryTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
registryId: this.id,
|
||||||
|
});
|
||||||
|
plugins.smartexpect.expect(response.ok).toBeTrue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,113 @@
|
|||||||
|
import type { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export class Image {
|
export class Image implements plugins.servezoneInterfaces.data.IImage {
|
||||||
public getImages() {}
|
public static async getImages(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getAllImagesTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
||||||
|
'getAllImages'
|
||||||
|
);
|
||||||
|
const response = await getAllImagesTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const resultImages: Image[] = [];
|
||||||
|
for (const image of response.images) {
|
||||||
|
const newImage = new Image(cloudlyClientRef);
|
||||||
|
Object.assign(newImage, image);
|
||||||
|
resultImages.push(newImage);
|
||||||
|
}
|
||||||
|
return resultImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getImageById(cloudlyClientRef: CloudlyApiClient, imageIdArg: string) {
|
||||||
|
const getImageByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
||||||
|
'getImage'
|
||||||
|
);
|
||||||
|
const response = await getImageByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
imageId: imageIdArg,
|
||||||
|
});
|
||||||
|
const newImage = new Image(cloudlyClientRef);
|
||||||
|
Object.assign(newImage, response.image);
|
||||||
|
return newImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a new image
|
||||||
|
*/
|
||||||
|
public static async createImage(cloudlyClientRef: CloudlyApiClient, imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>) {
|
||||||
|
const createImageTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
||||||
|
'createImage'
|
||||||
|
);
|
||||||
|
const response = await createImageTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
name: imageDataArg.name,
|
||||||
|
description: imageDataArg.description,
|
||||||
|
});
|
||||||
|
const newImage = new Image(cloudlyClientRef);
|
||||||
|
Object.assign(newImage, response.image);
|
||||||
|
return newImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
id: plugins.servezoneInterfaces.data.IImage['id'];
|
||||||
|
data: plugins.servezoneInterfaces.data.IImage['data'];
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updates the image data
|
||||||
|
*/
|
||||||
|
public async update() {
|
||||||
|
const getVersionsTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
||||||
|
'getImage'
|
||||||
|
);
|
||||||
|
const response = await getVersionsTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
imageId: this.id,
|
||||||
|
});
|
||||||
|
Object.assign(this, response.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pushes a new version of the image
|
||||||
|
* @param imageVersion
|
||||||
|
* @param imageReadableArg
|
||||||
|
*/
|
||||||
|
public async pushImageVersion(imageVersion: string, imageReadableArg: ReadableStream<Uint8Array>): Promise<void> {
|
||||||
|
const done = plugins.smartpromise.defer();
|
||||||
|
const pushImageTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_PushImageVersion>(
|
||||||
|
'pushImageVersion'
|
||||||
|
);
|
||||||
|
const virtualStream = new plugins.typedrequest.VirtualStream();
|
||||||
|
const response = await pushImageTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
imageId: this.id,
|
||||||
|
versionString: '',
|
||||||
|
imageStream: virtualStream,
|
||||||
|
});
|
||||||
|
await virtualStream.readFromWebstream(imageReadableArg);
|
||||||
|
await this.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pulls a version of the image
|
||||||
|
*/
|
||||||
|
public async pullImageVersion(versionStringArg: string): Promise<ReadableStream<Uint8Array>> {
|
||||||
|
const pullImageTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_PullImageVersion>(
|
||||||
|
'pullImageVersion'
|
||||||
|
);
|
||||||
|
const response = await pullImageTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
imageId: this.id,
|
||||||
|
versionString: versionStringArg,
|
||||||
|
});
|
||||||
|
const imageStream = response.imageStream;
|
||||||
|
const webduplexStream = new plugins.webstream.WebDuplexStream({});
|
||||||
|
imageStream.writeToWebstream(webduplexStream.writable);
|
||||||
|
return webduplexStream.readable;
|
||||||
|
};
|
||||||
}
|
}
|
135
ts_apiclient/classes.secretbundle.ts
Normal file
135
ts_apiclient/classes.secretbundle.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import type { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
|
import { SecretGroup } from './classes.secretgroup.js';
|
||||||
|
|
||||||
|
export class SecretBundle implements plugins.servezoneInterfaces.data.ISecretBundle {
|
||||||
|
// STATIC
|
||||||
|
public static async getSecretBundleById(cloudlyClientRef: CloudlyApiClient, secretBundleIdArg: string) {
|
||||||
|
const getSecretBundleByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleById>(
|
||||||
|
'getSecretBundleById'
|
||||||
|
);
|
||||||
|
const response = await getSecretBundleByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretBundleId: secretBundleIdArg,
|
||||||
|
});
|
||||||
|
const newSecretBundle = new SecretBundle(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretBundle, response.secretBundle);
|
||||||
|
return newSecretBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getSecretBundleByAuthorization(cloudlyClientRef: CloudlyApiClient, secretBundleAuthorizationArg: plugins.servezoneInterfaces.data.ISecretBundleAuthorization) {
|
||||||
|
const getSecretBundleByAuthorizationTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleByAuthorization>(
|
||||||
|
'getSecretBundleByAuthorization'
|
||||||
|
);
|
||||||
|
const response = await getSecretBundleByAuthorizationTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretBundleAuthorization: secretBundleAuthorizationArg,
|
||||||
|
});
|
||||||
|
const newSecretBundle = new SecretBundle(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretBundle, response.secretBundle);
|
||||||
|
return newSecretBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getSecretBundles(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getSecretBundlesTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundles>(
|
||||||
|
'getSecretBundles'
|
||||||
|
);
|
||||||
|
const response = await getSecretBundlesTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const secretBundles: SecretBundle[] = [];
|
||||||
|
for (const secretBundle of response.secretBundles) {
|
||||||
|
const newSecretBundle = new SecretBundle(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretBundle, secretBundle);
|
||||||
|
secretBundles.push(newSecretBundle);
|
||||||
|
}
|
||||||
|
return secretBundles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createSecretBundle(cloudlyClientRef: CloudlyApiClient, secretBundleDataArg: Partial<plugins.servezoneInterfaces.data.ISecretBundle['data']>) {
|
||||||
|
const createSecretBundleTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_CreateSecretBundle>(
|
||||||
|
'createSecretBundle'
|
||||||
|
);
|
||||||
|
const response = await createSecretBundleTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretBundle: {
|
||||||
|
id: null,
|
||||||
|
data: {
|
||||||
|
name: secretBundleDataArg.name,
|
||||||
|
description: secretBundleDataArg.description,
|
||||||
|
type: secretBundleDataArg.type,
|
||||||
|
authorizations: secretBundleDataArg.authorizations,
|
||||||
|
imageClaims: secretBundleDataArg.imageClaims,
|
||||||
|
includedSecretGroupIds: secretBundleDataArg.includedSecretGroupIds,
|
||||||
|
includedTags: secretBundleDataArg.includedTags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const newSecretBundle = new SecretBundle(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretBundle, response.resultSecretBundle);
|
||||||
|
return newSecretBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
|
||||||
|
public cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
public id: string;
|
||||||
|
public data: plugins.servezoneInterfaces.data.ISecretBundle['data'];
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update() {
|
||||||
|
const updateSecretBundleTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_UpdateSecretBundle>(
|
||||||
|
'updateSecretBundle'
|
||||||
|
);
|
||||||
|
const response = await updateSecretBundleTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
secretBundle: {
|
||||||
|
id: this.id,
|
||||||
|
data: this.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultSecretBundleData = response.resultSecretBundle.data;
|
||||||
|
plugins.smartexpect.expect(resultSecretBundleData).toEqual(this.data);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(cloudlyClientRef: CloudlyApiClient, secretBundleIdArg: string) {
|
||||||
|
const deleteSecretBundleTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(
|
||||||
|
'deleteSecretBundleById'
|
||||||
|
);
|
||||||
|
const response = await deleteSecretBundleTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretBundleId: this.id,
|
||||||
|
});
|
||||||
|
plugins.smartexpect.expect(response.ok).toBeTrue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFlatKeyValueObjectForEnvironment(environmentArg: string = 'production') {
|
||||||
|
const bundleAuthorization = this.data.authorizations.find(authorization => {
|
||||||
|
return authorization.environment === environmentArg;
|
||||||
|
});
|
||||||
|
if (bundleAuthorization) {
|
||||||
|
throw new Error(`no matching environment >>${environmentArg} found in secret bundle`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFlatKeyValueObjectTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetFlatKeyValueObject>(
|
||||||
|
'getFlatKeyValueObject'
|
||||||
|
);
|
||||||
|
const response = await getFlatKeyValueObjectTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
seccretBundleId: this.id,
|
||||||
|
secretBundleAuthorization: bundleAuthorization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const flatKeyValueObject: {[key: string]: string} = response.flatKeyValueObject;
|
||||||
|
|
||||||
|
return flatKeyValueObject;
|
||||||
|
}
|
||||||
|
}
|
96
ts_apiclient/classes.secretgroup.ts
Normal file
96
ts_apiclient/classes.secretgroup.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import type { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
|
|
||||||
|
export class SecretGroup implements plugins.servezoneInterfaces.data.ISecretGroup {
|
||||||
|
public cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
public id: string;
|
||||||
|
public data: plugins.servezoneInterfaces.data.ISecretGroup['data'];
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getSecretGroupById(cloudlyClientRef: CloudlyApiClient, secretGroupIdArg: string) {
|
||||||
|
const getSecretGroupByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_GetSecretGroupById>(
|
||||||
|
'getSecretGroupById'
|
||||||
|
);
|
||||||
|
const response = await getSecretGroupByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretGroupId: secretGroupIdArg,
|
||||||
|
});
|
||||||
|
const newSecretGroup = new SecretGroup(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretGroup, response.secretGroup);
|
||||||
|
return newSecretGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getSecretGroups(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getSecretGroupsTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_GetSecretGroups>(
|
||||||
|
'getSecretGroups'
|
||||||
|
);
|
||||||
|
const response = await getSecretGroupsTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const secretGroups: SecretGroup[] = [];
|
||||||
|
for (const secretGroup of response.secretGroups) {
|
||||||
|
const newSecretGroup = new SecretGroup(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretGroup, secretGroup);
|
||||||
|
secretGroups.push(newSecretGroup);
|
||||||
|
}
|
||||||
|
return secretGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createSecretGroup(cloudlyClientRef: CloudlyApiClient, secretGroupDataArg: Partial<plugins.servezoneInterfaces.data.ISecretGroup['data']>) {
|
||||||
|
const createSecretGroupTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_CreateSecretGroup>(
|
||||||
|
'createSecretGroup'
|
||||||
|
);
|
||||||
|
const response = await createSecretGroupTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretGroup: {
|
||||||
|
id: null,
|
||||||
|
data: {
|
||||||
|
name: secretGroupDataArg.name,
|
||||||
|
description: secretGroupDataArg.description,
|
||||||
|
environments: secretGroupDataArg.environments,
|
||||||
|
key: secretGroupDataArg.key,
|
||||||
|
tags: secretGroupDataArg.tags,
|
||||||
|
priority: secretGroupDataArg.priority,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const newSecretGroup = new SecretGroup(cloudlyClientRef);
|
||||||
|
Object.assign(newSecretGroup, response.resultSecretGroup);
|
||||||
|
return newSecretGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public async update() {
|
||||||
|
const updateSecretGroupTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_UpdateSecretGroup>(
|
||||||
|
'updateSecretGroup'
|
||||||
|
);
|
||||||
|
const response = await updateSecretGroupTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
secretGroup: {
|
||||||
|
id: this.id,
|
||||||
|
data: this.data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultSecretGroupData = response.resultSecretGroup.data;
|
||||||
|
plugins.smartexpect.expect(resultSecretGroupData).toEqual(this.data);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(cloudlyClientRef: CloudlyApiClient, secretGroupIdArg: string) {
|
||||||
|
const deleteSecretGroupTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(
|
||||||
|
'deleteSecretGroupById'
|
||||||
|
);
|
||||||
|
const response = await deleteSecretGroupTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
secretGroupId: this.id,
|
||||||
|
});
|
||||||
|
plugins.smartexpect.expect(response.ok).toBeTrue();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
public static getServers() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,78 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import type { CloudlyApiClient } from './classes.cloudlyapiclient.js';
|
||||||
|
|
||||||
export class Service {
|
export class Service implements plugins.servezoneInterfaces.data.IService {
|
||||||
|
public static async getServices(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
const getAllServicesTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
||||||
|
'getServices'
|
||||||
|
);
|
||||||
|
const response = await getAllServicesTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
});
|
||||||
|
const resultServices: Service[] = [];
|
||||||
|
for (const service of response.services) {
|
||||||
|
const newService = new Service(cloudlyClientRef);
|
||||||
|
Object.assign(newService, service);
|
||||||
|
resultServices.push(newService);
|
||||||
|
}
|
||||||
|
return resultServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getServiceById(cloudlyClientRef: CloudlyApiClient, serviceIdArg: string) {
|
||||||
|
const getServiceByIdTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceById>(
|
||||||
|
'getServiceById'
|
||||||
|
);
|
||||||
|
const response = await getServiceByIdTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
serviceId: serviceIdArg,
|
||||||
|
});
|
||||||
|
const newService = new Service(cloudlyClientRef);
|
||||||
|
Object.assign(newService, response.service);
|
||||||
|
return newService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a new service
|
||||||
|
*/
|
||||||
|
public static async createService(cloudlyClientRef: CloudlyApiClient, serviceDataArg: Partial<plugins.servezoneInterfaces.data.IService['data']>) {
|
||||||
|
const createServiceTR = cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
||||||
|
'createService'
|
||||||
|
);
|
||||||
|
const response = await createServiceTR.fire({
|
||||||
|
identity: cloudlyClientRef.identity,
|
||||||
|
serviceData: serviceDataArg as plugins.servezoneInterfaces.data.IService['data'],
|
||||||
|
});
|
||||||
|
const newService = new Service(cloudlyClientRef);
|
||||||
|
Object.assign(newService, response.service);
|
||||||
|
return newService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
cloudlyClientRef: CloudlyApiClient;
|
||||||
|
|
||||||
|
public id: string;
|
||||||
|
public data: plugins.servezoneInterfaces.data.IService['data'];
|
||||||
|
|
||||||
|
constructor(cloudlyClientRef: CloudlyApiClient) {
|
||||||
|
this.cloudlyClientRef = cloudlyClientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service has a secret bundle.
|
||||||
|
* This function essentially returns the secret bundle as a flat object.
|
||||||
|
* In other words, it resolves secret groups and
|
||||||
|
*/
|
||||||
|
public async getSecretBundleAsFlatObject(environmentArg: string = 'production') {
|
||||||
|
const getServiceSecretBundlesAsFlatObjectTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject>(
|
||||||
|
'getServiceSecretBundlesAsFlatObject'
|
||||||
|
);
|
||||||
|
const response = await getServiceSecretBundlesAsFlatObjectTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
serviceId: this.id,
|
||||||
|
environment: environmentArg,
|
||||||
|
});
|
||||||
|
const flatKeyValueObject: {[key: string]: string} = response.flatKeyValueObject;
|
||||||
|
|
||||||
|
return flatKeyValueObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
export * from './classes.cloudlyclient.js';
|
export * from './classes.cloudlyapiclient.js';
|
@@ -6,10 +6,16 @@ export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
|
import * as smartexpect from '@push.rocks/smartexpect';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
|
import * as webstream from '@push.rocks/smartstream/web';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
smartexpect,
|
||||||
|
smartpromise,
|
||||||
smartrx,
|
smartrx,
|
||||||
|
webstream,
|
||||||
}
|
}
|
||||||
|
|
||||||
// @api.global scope
|
// @api.global scope
|
||||||
|
306
ts_apiclient/readme.md
Normal file
306
ts_apiclient/readme.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# @serve.zone/api 🔌
|
||||||
|
|
||||||
|
**The powerful API client for Cloudly.** Connect your applications to multi-cloud infrastructure with type-safe, real-time communication.
|
||||||
|
|
||||||
|
## 🎯 What is @serve.zone/api?
|
||||||
|
|
||||||
|
This is your programmatic gateway to the Cloudly platform. Built with TypeScript, it provides a robust, type-safe interface for managing cloud resources, orchestrating containers, and automating infrastructure operations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🔒 Type-Safe** - Full TypeScript support with comprehensive interfaces
|
||||||
|
- **⚡ Real-Time** - WebSocket-based communication for instant updates
|
||||||
|
- **🔑 Secure Authentication** - Token-based identity management
|
||||||
|
- **📦 Resource Management** - Complete control over clusters, images, and services
|
||||||
|
- **🎭 Multi-Identity** - Support for service accounts and user authentication
|
||||||
|
- **🔄 Reactive Streams** - RxJS integration for event-driven programming
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎬 Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
|
// Initialize the client
|
||||||
|
const client = new CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: 'https://cloudly.example.com:443'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the connection
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
// Authenticate with a service token
|
||||||
|
const identity = await client.getIdentityByToken('your-service-token', {
|
||||||
|
tagConnection: true,
|
||||||
|
statefullIdentity: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🎉 Connected as:', identity.name);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Authentication
|
||||||
|
|
||||||
|
### Service Token Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Authenticate using a service token
|
||||||
|
const identity = await client.getIdentityByToken(serviceToken, {
|
||||||
|
tagConnection: true, // Tag this connection with the identity
|
||||||
|
statefullIdentity: true // Maintain state across reconnections
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Identity Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get current identity
|
||||||
|
const currentIdentity = client.identity;
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
if (currentIdentity.permissions.includes('cluster:write')) {
|
||||||
|
// Perform cluster operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Core Operations
|
||||||
|
|
||||||
|
### 🐳 Image Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create an image entry
|
||||||
|
const image = await client.images.createImage({
|
||||||
|
name: 'my-app',
|
||||||
|
description: 'Production application image'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push a new version
|
||||||
|
const imageStream = fs.createReadStream('app.tar');
|
||||||
|
await image.pushImageVersion('2.0.0', imageStream);
|
||||||
|
|
||||||
|
// List all images
|
||||||
|
const images = await client.images.listImages();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 Cluster Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get cluster configuration
|
||||||
|
const clusterConfig = await client.getClusterConfigFromCloudlyByIdentity(identity);
|
||||||
|
|
||||||
|
// Deploy to cluster
|
||||||
|
await client.deployToCluster({
|
||||||
|
clusterName: 'production',
|
||||||
|
serviceName: 'api-service',
|
||||||
|
image: 'my-app:2.0.0',
|
||||||
|
replicas: 3
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Certificate Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Request SSL certificate
|
||||||
|
const certificate = await client.getCertificateForDomain({
|
||||||
|
domainName: 'api.example.com',
|
||||||
|
type: 'ssl',
|
||||||
|
identity: identity
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use certificate in your application
|
||||||
|
console.log('Certificate:', certificate.cert);
|
||||||
|
console.log('Private Key:', certificate.key);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Secret Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create secret group
|
||||||
|
const secretGroup = await client.secrets.createSecretGroup({
|
||||||
|
name: 'api-secrets',
|
||||||
|
secrets: [
|
||||||
|
{ key: 'DATABASE_URL', value: 'postgres://...' },
|
||||||
|
{ key: 'REDIS_URL', value: 'redis://...' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retrieve secrets
|
||||||
|
const secrets = await client.secrets.getSecretGroup('api-secrets');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Real-Time Updates
|
||||||
|
|
||||||
|
Subscribe to configuration changes and server actions using RxJS:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Listen for configuration updates
|
||||||
|
client.configUpdateSubject.subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
console.log('📡 Configuration updated:', config);
|
||||||
|
// React to configuration changes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle server action requests
|
||||||
|
client.serverActionSubject.subscribe({
|
||||||
|
next: (action) => {
|
||||||
|
console.log('⚡ Server action:', action.type);
|
||||||
|
// Process server-initiated actions
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Usage
|
||||||
|
|
||||||
|
### Streaming Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stream logs from a service
|
||||||
|
const logStream = await client.logs.streamLogs({
|
||||||
|
service: 'api-service',
|
||||||
|
follow: true
|
||||||
|
});
|
||||||
|
|
||||||
|
logStream.on('data', (log) => {
|
||||||
|
console.log(log.message);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Deploy multiple services
|
||||||
|
const deployments = await Promise.all([
|
||||||
|
client.deploy({ service: 'frontend', image: 'app:latest' }),
|
||||||
|
client.deploy({ service: 'backend', image: 'api:latest' }),
|
||||||
|
client.deploy({ service: 'worker', image: 'worker:latest' })
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'AUTH_FAILED') {
|
||||||
|
console.error('Authentication failed:', error.message);
|
||||||
|
} else if (error.code === 'CONNECTION_LOST') {
|
||||||
|
console.error('Connection lost, retrying...');
|
||||||
|
await client.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧹 Cleanup
|
||||||
|
|
||||||
|
Always gracefully disconnect when done:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stop the client connection
|
||||||
|
await client.stop();
|
||||||
|
console.log('✅ Disconnected cleanly');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 API Reference
|
||||||
|
|
||||||
|
### CloudlyApiClient
|
||||||
|
|
||||||
|
Main client class for interacting with Cloudly.
|
||||||
|
|
||||||
|
#### Constructor Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ICloudlyApiClientOptions {
|
||||||
|
registerAs: TClientType; // 'api' | 'cli' | 'web'
|
||||||
|
cloudlyUrl: string; // Full URL including protocol and port
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
- `start()` - Initialize connection
|
||||||
|
- `stop()` - Close connection
|
||||||
|
- `getIdentityByToken()` - Authenticate with token
|
||||||
|
- `getClusterConfigFromCloudlyByIdentity()` - Get cluster configuration
|
||||||
|
- `getCertificateForDomain()` - Request SSL certificate
|
||||||
|
- `images` - Image management namespace
|
||||||
|
- `secrets` - Secret management namespace
|
||||||
|
- `clusters` - Cluster management namespace
|
||||||
|
|
||||||
|
## 🎬 Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Initialize client
|
||||||
|
const client = new CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: 'https://cloudly.example.com:443'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect and authenticate
|
||||||
|
await client.start();
|
||||||
|
const identity = await client.getIdentityByToken(process.env.SERVICE_TOKEN, {
|
||||||
|
tagConnection: true,
|
||||||
|
statefullIdentity: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and deploy an image
|
||||||
|
const image = await client.images.createImage({
|
||||||
|
name: 'my-service',
|
||||||
|
description: 'Microservice application'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push image version
|
||||||
|
const stream = getImageStream(); // Your image stream
|
||||||
|
await image.pushImageVersion('1.0.0', stream);
|
||||||
|
|
||||||
|
// Deploy to cluster
|
||||||
|
await client.deployToCluster({
|
||||||
|
clusterName: 'production',
|
||||||
|
serviceName: 'my-service',
|
||||||
|
image: 'my-service:1.0.0',
|
||||||
|
replicas: 3,
|
||||||
|
environment: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Deployment successful!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
} finally {
|
||||||
|
await client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
17
ts_apiclient/tspublish.json
Normal file
17
ts_apiclient/tspublish.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"name": "@serve.zone/api",
|
||||||
|
"dependencies": [
|
||||||
|
"@serve.zone/interfaces",
|
||||||
|
"@push.rocks/smartpromise",
|
||||||
|
"@push.rocks/smartrx",
|
||||||
|
"@push.rocks/smartstream",
|
||||||
|
"@api.global/typedrequest",
|
||||||
|
"@api.global/typedsocket",
|
||||||
|
"@tsclass/tsclass"
|
||||||
|
],
|
||||||
|
"registries": [
|
||||||
|
"registry.npmjs.org:public",
|
||||||
|
"verdaccio.lossless.digital:public"
|
||||||
|
]
|
||||||
|
}
|
15
ts_cliclient/classes.cliclient.ts
Normal file
15
ts_cliclient/classes.cliclient.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
|
export class CliClient {
|
||||||
|
public cloudlyApiClient: CloudlyApiClient;
|
||||||
|
|
||||||
|
constructor(cloudlyApiClientArg: CloudlyApiClient) {
|
||||||
|
this.cloudlyApiClient = cloudlyApiClientArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getClusters() {
|
||||||
|
const clusters = await this.cloudlyApiClient.cluster.getClusters();
|
||||||
|
console.log(clusters);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { CliClient } from "./classes.cliclient.js";
|
||||||
|
|
||||||
|
export const runCli = async () => {
|
||||||
|
const cliQenv = new plugins.qenv.Qenv();
|
||||||
|
const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
||||||
|
registerAs: 'cli',
|
||||||
|
cloudlyUrl: await cliQenv.getEnvVarOnDemand('CLOUDLY_URL'),
|
||||||
|
});
|
||||||
|
const cliClient = new CliClient(apiClient);
|
||||||
|
};
|
17
ts_cliclient/plugins.ts
Normal file
17
ts_cliclient/plugins.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// @serve.zone scope
|
||||||
|
import * as servezoneApi from '@serve.zone/api';
|
||||||
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
servezoneApi,
|
||||||
|
servezoneInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// @push.rocks scope
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as qenv from '@push.rocks/qenv';
|
||||||
|
|
||||||
|
export {
|
||||||
|
projectinfo,
|
||||||
|
qenv,
|
||||||
|
}
|
358
ts_cliclient/readme.md
Normal file
358
ts_cliclient/readme.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# @serve.zone/cli 🚀
|
||||||
|
|
||||||
|
**Command-line interface for Cloudly.** Manage your multi-cloud infrastructure from the terminal with powerful, intuitive commands.
|
||||||
|
|
||||||
|
## 🎯 What is @serve.zone/cli?
|
||||||
|
|
||||||
|
The Cloudly CLI brings the full power of the Cloudly platform to your terminal. Whether you're automating deployments, managing secrets, or monitoring services, the CLI provides a streamlined interface for all your cloud operations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **⚡ Fast & Efficient** - Optimized for speed and minimal resource usage
|
||||||
|
- **🔐 Secure Authentication** - Token-based authentication with secure storage
|
||||||
|
- **📝 Intuitive Commands** - Clear, consistent command structure
|
||||||
|
- **🎨 Formatted Output** - Beautiful, readable output with color coding
|
||||||
|
- **🔄 Scriptable** - Perfect for CI/CD pipelines and automation
|
||||||
|
- **📊 Comprehensive** - Access to all Cloudly features from the terminal
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Global Installation (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -g @serve.zone/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎬 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure your Cloudly instance
|
||||||
|
servezone config --url https://cloudly.example.com
|
||||||
|
|
||||||
|
# Login with your service token
|
||||||
|
servezone login --token your-service-token
|
||||||
|
|
||||||
|
# List your clusters
|
||||||
|
servezone clusters list
|
||||||
|
|
||||||
|
# Deploy a service
|
||||||
|
servezone deploy --cluster production --image myapp:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Authentication
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set your Cloudly instance URL
|
||||||
|
servezone config --url https://cloudly.example.com
|
||||||
|
|
||||||
|
# Authenticate with a service token
|
||||||
|
servezone login --token YOUR_SERVICE_TOKEN
|
||||||
|
|
||||||
|
# Or use environment variables
|
||||||
|
export CLOUDLY_URL=https://cloudly.example.com
|
||||||
|
export CLOUDLY_TOKEN=YOUR_SERVICE_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Profiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a profile for different environments
|
||||||
|
servezone profile create production --url https://prod.cloudly.com
|
||||||
|
servezone profile create staging --url https://stage.cloudly.com
|
||||||
|
|
||||||
|
# Switch between profiles
|
||||||
|
servezone profile use production
|
||||||
|
|
||||||
|
# List all profiles
|
||||||
|
servezone profile list
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Core Commands
|
||||||
|
|
||||||
|
### 🌐 Cluster Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all clusters
|
||||||
|
servezone clusters list
|
||||||
|
|
||||||
|
# Get cluster details
|
||||||
|
servezone clusters info production-cluster
|
||||||
|
|
||||||
|
# Create a new cluster
|
||||||
|
servezone clusters create \
|
||||||
|
--name production-cluster \
|
||||||
|
--region eu-central \
|
||||||
|
--nodes 3
|
||||||
|
|
||||||
|
# Scale a cluster
|
||||||
|
servezone clusters scale production-cluster --nodes 5
|
||||||
|
|
||||||
|
# Delete a cluster
|
||||||
|
servezone clusters delete staging-cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🐳 Service Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy a service
|
||||||
|
servezone deploy \
|
||||||
|
--cluster production \
|
||||||
|
--name api-service \
|
||||||
|
--image myapp:2.0.0 \
|
||||||
|
--replicas 3 \
|
||||||
|
--port 80:3000
|
||||||
|
|
||||||
|
# Update a service
|
||||||
|
servezone service update api-service \
|
||||||
|
--image myapp:2.1.0 \
|
||||||
|
--replicas 5
|
||||||
|
|
||||||
|
# Scale a service
|
||||||
|
servezone service scale api-service --replicas 10
|
||||||
|
|
||||||
|
# Remove a service
|
||||||
|
servezone service remove api-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Secret Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a secret
|
||||||
|
servezone secrets create \
|
||||||
|
--name database-url \
|
||||||
|
--value "postgres://user:pass@host/db"
|
||||||
|
|
||||||
|
# Create a secret group
|
||||||
|
servezone secrets create-group \
|
||||||
|
--name api-secrets \
|
||||||
|
--secret DATABASE_URL=postgres://... \
|
||||||
|
--secret REDIS_URL=redis://...
|
||||||
|
|
||||||
|
# List secrets
|
||||||
|
servezone secrets list
|
||||||
|
|
||||||
|
# Get secret value
|
||||||
|
servezone secrets get database-url
|
||||||
|
|
||||||
|
# Delete a secret
|
||||||
|
servezone secrets delete old-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Image Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List images
|
||||||
|
servezone images list
|
||||||
|
|
||||||
|
# Push a new image
|
||||||
|
servezone images push \
|
||||||
|
--name myapp \
|
||||||
|
--version 2.0.0 \
|
||||||
|
--file ./myapp.tar
|
||||||
|
|
||||||
|
# Tag an image
|
||||||
|
servezone images tag myapp:2.0.0 myapp:latest
|
||||||
|
|
||||||
|
# Delete an image
|
||||||
|
servezone images delete myapp:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Monitoring & Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View service logs
|
||||||
|
servezone logs api-service
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
servezone logs api-service --follow
|
||||||
|
|
||||||
|
# Filter logs
|
||||||
|
servezone logs api-service --since 1h --grep ERROR
|
||||||
|
|
||||||
|
# Get service status
|
||||||
|
servezone service status api-service
|
||||||
|
|
||||||
|
# Monitor cluster health
|
||||||
|
servezone clusters health production-cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 DNS Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List DNS records
|
||||||
|
servezone dns list --domain example.com
|
||||||
|
|
||||||
|
# Create a DNS record
|
||||||
|
servezone dns create \
|
||||||
|
--domain example.com \
|
||||||
|
--name api \
|
||||||
|
--type A \
|
||||||
|
--value 192.168.1.1
|
||||||
|
|
||||||
|
# Update a DNS record
|
||||||
|
servezone dns update api.example.com --value 192.168.1.2
|
||||||
|
|
||||||
|
# Delete a DNS record
|
||||||
|
servezone dns delete old.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Usage
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables for a service
|
||||||
|
servezone deploy \
|
||||||
|
--name api-service \
|
||||||
|
--env NODE_ENV=production \
|
||||||
|
--env PORT=3000 \
|
||||||
|
--env DATABASE_URL=@secret:database-url
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
Create a `cloudly.yaml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cluster: production
|
||||||
|
service:
|
||||||
|
name: api-service
|
||||||
|
image: myapp:latest
|
||||||
|
replicas: 3
|
||||||
|
ports:
|
||||||
|
- 80:3000
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DATABASE_URL: "@secret:database-url"
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy using the config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
servezone deploy --config cloudly.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy multiple services
|
||||||
|
servezone deploy --config services/*.yaml
|
||||||
|
|
||||||
|
# Update all services in a namespace
|
||||||
|
servezone service update --namespace api --image-tag v2.0.0
|
||||||
|
|
||||||
|
# Delete all staging resources
|
||||||
|
servezone cleanup --environment staging
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Deploy to Cloudly
|
||||||
|
run: |
|
||||||
|
servezone config --url ${{ secrets.CLOUDLY_URL }}
|
||||||
|
servezone login --token ${{ secrets.CLOUDLY_TOKEN }}
|
||||||
|
servezone deploy \
|
||||||
|
--cluster production \
|
||||||
|
--name api-service \
|
||||||
|
--image myapp:${{ github.sha }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
script:
|
||||||
|
- servezone config --url $CLOUDLY_URL
|
||||||
|
- servezone login --token $CLOUDLY_TOKEN
|
||||||
|
- servezone deploy --config cloudly.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Output Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON output for scripting
|
||||||
|
servezone clusters list --output json
|
||||||
|
|
||||||
|
# YAML output
|
||||||
|
servezone service info api-service --output yaml
|
||||||
|
|
||||||
|
# Table output (default)
|
||||||
|
servezone images list --output table
|
||||||
|
|
||||||
|
# Quiet mode (IDs only)
|
||||||
|
servezone clusters list --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable debug output
|
||||||
|
servezone --debug clusters list
|
||||||
|
|
||||||
|
# Check CLI version
|
||||||
|
servezone version
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
servezone ping
|
||||||
|
|
||||||
|
# View configuration
|
||||||
|
servezone config show
|
||||||
|
|
||||||
|
# Clear cache and credentials
|
||||||
|
servezone logout --clear-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Command Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
servezone --help # Show all commands
|
||||||
|
servezone <command> --help # Show command-specific help
|
||||||
|
servezone clusters --help # Show cluster commands
|
||||||
|
servezone service --help # Show service commands
|
||||||
|
servezone secrets --help # Show secret commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Shell Completion
|
||||||
|
|
||||||
|
Enable tab completion for your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bash
|
||||||
|
servezone completion bash > /etc/bash_completion.d/servezone
|
||||||
|
|
||||||
|
# Zsh
|
||||||
|
servezone completion zsh > ~/.zsh/completions/_servezone
|
||||||
|
|
||||||
|
# Fish
|
||||||
|
servezone completion fish > ~/.config/fish/completions/servezone.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
15
ts_cliclient/tspublish.json
Normal file
15
ts_cliclient/tspublish.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@serve.zone/cli",
|
||||||
|
"dependencies": [
|
||||||
|
"@serve.zone/api",
|
||||||
|
"@serve.zone/interfaces",
|
||||||
|
"@push.rocks/projectinfo",
|
||||||
|
"@push.rocks/qenv",
|
||||||
|
"@push.rocks/smartcli"
|
||||||
|
],
|
||||||
|
"registries": [
|
||||||
|
"registry.npmjs.org:public",
|
||||||
|
"verdaccio.lossless.digital:public"
|
||||||
|
],
|
||||||
|
"bin": ["servezone"]
|
||||||
|
}
|
8
ts_interfaces/00_commitinfo_data.ts
Normal file
8
ts_interfaces/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@serve.zone/interfaces',
|
||||||
|
version: '1.1.2',
|
||||||
|
description: 'interfaces for working with containers'
|
||||||
|
}
|
16
ts_interfaces/data/cloudlyconfig.ts
Normal file
16
ts_interfaces/data/cloudlyconfig.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface ICloudlyConfig {
|
||||||
|
cfToken?: string;
|
||||||
|
hetznerToken?: string;
|
||||||
|
environment?: 'production' | 'integration';
|
||||||
|
letsEncryptEmail?: string;
|
||||||
|
letsEncryptPrivateKey?: string;
|
||||||
|
jwtKeypair?: plugins.tsclass.network.IJwtKeypair;
|
||||||
|
mongoDescriptor?: plugins.tsclass.database.IMongoDescriptor;
|
||||||
|
s3Descriptor?: plugins.tsclass.storage.IS3Descriptor;
|
||||||
|
publicUrl?: string;
|
||||||
|
publicPort?: string;
|
||||||
|
sslMode?: 'none' | 'letsencrypt' | 'external';
|
||||||
|
servezoneAdminaccount?: string;
|
||||||
|
}
|
36
ts_interfaces/data/cluster.ts
Normal file
36
ts_interfaces/data/cluster.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
import { type IDockerRegistryInfo } from '../data/docker.js';
|
||||||
|
import type { IServer } from './server.js';
|
||||||
|
|
||||||
|
export interface ICluster {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a cluster has a machine user that governs access rights.
|
||||||
|
*/
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* how can the cluster reach cloudly
|
||||||
|
*/
|
||||||
|
cloudlyUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* what servers are expected to be part of the cluster
|
||||||
|
*/
|
||||||
|
servers: IServer[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ACME info. This is used to get SSL certificates.
|
||||||
|
*/
|
||||||
|
acmeInfo: {
|
||||||
|
serverAddress: string;
|
||||||
|
serverSecret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
sshKeys: plugins.tsclass.network.ISshKey[];
|
||||||
|
};
|
||||||
|
}
|
1
ts_interfaces/data/config.ts
Normal file
1
ts_interfaces/data/config.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type TConfigType = 'server' | 'cluster' | 'coreflow' | 'service';
|
13
ts_interfaces/data/deployment.ts
Normal file
13
ts_interfaces/data/deployment.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a deployment happens when a service is deployed
|
||||||
|
* tracks the status of a deployment
|
||||||
|
*/
|
||||||
|
export interface IDeployment {
|
||||||
|
id: string;
|
||||||
|
affectedServiceIds: string[];
|
||||||
|
usedImageId: string;
|
||||||
|
deploymentLog: string[];
|
||||||
|
status: 'scheduled' | 'running' | 'deployed' | 'failed';
|
||||||
|
}
|
15
ts_interfaces/data/docker.ts
Normal file
15
ts_interfaces/data/docker.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IDockerRegistryInfo {
|
||||||
|
serveraddress: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IServiceRessources {
|
||||||
|
cpuLimit?: number;
|
||||||
|
cpuReservation?: number;
|
||||||
|
memorySizeLimitMB?: number;
|
||||||
|
memorySizeReservationMB?: number;
|
||||||
|
volumeMounts?: plugins.tsclass.container.IVolumeMount[];
|
||||||
|
}
|
11
ts_interfaces/data/event.ts
Normal file
11
ts_interfaces/data/event.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IEvent_Cloudly_ContainerVersionNotification
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedEvent<plugins.tsclass.container.IContainer>,
|
||||||
|
IEvent_Cloudly_ContainerVersionNotification
|
||||||
|
> {
|
||||||
|
name: 'newContainerVersion';
|
||||||
|
uniqueEventId: string;
|
||||||
|
payload: plugins.tsclass.container.IContainer;
|
||||||
|
}
|
12
ts_interfaces/data/externalregistry.ts
Normal file
12
ts_interfaces/data/externalregistry.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IExternalRegistry {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
type: 'docker' | 'npm';
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
20
ts_interfaces/data/image.ts
Normal file
20
ts_interfaces/data/image.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IImage {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
location: {
|
||||||
|
internal: boolean;
|
||||||
|
externalRegistryId: string;
|
||||||
|
externalImageTag: string;
|
||||||
|
}
|
||||||
|
description: string;
|
||||||
|
versions: Array<{
|
||||||
|
versionString: string;
|
||||||
|
storagePath?: string;
|
||||||
|
size: number;
|
||||||
|
createdAt: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
16
ts_interfaces/data/index.ts
Normal file
16
ts_interfaces/data/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export * from './cloudlyconfig.js';
|
||||||
|
export * from './cluster.js';
|
||||||
|
export * from './config.js';
|
||||||
|
export * from './deployment.js';
|
||||||
|
export * from './docker.js';
|
||||||
|
export * from './event.js';
|
||||||
|
export * from './externalregistry.js';
|
||||||
|
export * from './image.js';
|
||||||
|
export * from './secretbundle.js';
|
||||||
|
export * from './secretgroup.js'
|
||||||
|
export * from './server.js';
|
||||||
|
export * from './service.js';
|
||||||
|
export * from './status.js';
|
||||||
|
export * from './traffic.js';
|
||||||
|
export * from './user.js';
|
||||||
|
export * from './version.js';
|
61
ts_interfaces/data/secretbundle.ts
Normal file
61
ts_interfaces/data/secretbundle.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
export interface ISecretBundle {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determines if the secret is a service or an external secret
|
||||||
|
* if external secret additional checks are put in place to protect the secret
|
||||||
|
*
|
||||||
|
* * service:
|
||||||
|
* the bundle belongs to a service and can only be used by that service
|
||||||
|
* * npmci:
|
||||||
|
* the bundle is a secret bundle that is used by an npmci pipeline
|
||||||
|
* production secrets will be omitted in any case
|
||||||
|
* * gitzone:
|
||||||
|
* the bundle is a secret bundle that is used by a gitzone.
|
||||||
|
* Only local environment variables are allowed
|
||||||
|
* * external:
|
||||||
|
* the bundle is a secret bundle that is used by an external service
|
||||||
|
*/
|
||||||
|
type: 'service' | 'npmci' | 'gitzone' | 'external';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set this if the secretBundle belongs to a service
|
||||||
|
*/
|
||||||
|
serviceId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can add specific secret groups using this
|
||||||
|
*/
|
||||||
|
includedSecretGroupIds: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access to this secretBundle also grants access to resources with matching tags
|
||||||
|
*/
|
||||||
|
includedTags: {
|
||||||
|
key: string;
|
||||||
|
value?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access to this secretBundle also grants access to the images
|
||||||
|
*/
|
||||||
|
imageClaims: {
|
||||||
|
imageId: string;
|
||||||
|
permissions: ('read' | 'write')[];
|
||||||
|
}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* authrozations select a specific environment of a config bundle
|
||||||
|
*/
|
||||||
|
authorizations: Array<ISecretBundleAuthorization>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecretBundleAuthorization {
|
||||||
|
secretAccessKey: string;
|
||||||
|
environment: string;
|
||||||
|
}
|
54
ts_interfaces/data/secretgroup.ts
Normal file
54
ts_interfaces/data/secretgroup.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export interface ISecretGroup {
|
||||||
|
/**
|
||||||
|
* the insatnce id. This should be a random id, except for default
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the key of the secretgroup like CI_RUNNER_TOKEN
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the priority of the secretgroup
|
||||||
|
* will be used to determine which secretgroup will be used
|
||||||
|
* when there are multiple secretgroups with the same key
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* any tags that can be used to filter the secretgroup
|
||||||
|
* can be used for putting secrets into projects
|
||||||
|
*/
|
||||||
|
tags: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
/**
|
||||||
|
* the values for this secretGroup
|
||||||
|
*/
|
||||||
|
environments: {
|
||||||
|
[key: string]: {
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* can be used to update the value
|
||||||
|
*/
|
||||||
|
updateToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the linux timestamp of the last update
|
||||||
|
*/
|
||||||
|
lastUpdated: number;
|
||||||
|
history: {
|
||||||
|
timestamp: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
36
ts_interfaces/data/server.ts
Normal file
36
ts_interfaces/data/server.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
import { type IDockerRegistryInfo } from './docker.js';
|
||||||
|
|
||||||
|
export interface IServerMetrics {
|
||||||
|
serverId: string;
|
||||||
|
cpuUsageInPercent: number;
|
||||||
|
memoryUsageinMB: number;
|
||||||
|
memoryAvailableInMB: number;
|
||||||
|
containerCount: number;
|
||||||
|
containerMetrics: Array<{
|
||||||
|
containerId: string;
|
||||||
|
containerName: string;
|
||||||
|
cpuUsageInPercent: number;
|
||||||
|
memoryUsageInMB: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IServer {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
type: 'baremetal' | 'hetzner';
|
||||||
|
|
||||||
|
assignedClusterId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of debian packages to be installed
|
||||||
|
*/
|
||||||
|
requiredDebianPackages: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of SSH keys to deploy
|
||||||
|
*/
|
||||||
|
sshKeys: plugins.tsclass.network.ISshKey[];
|
||||||
|
};
|
||||||
|
}
|
34
ts_interfaces/data/service.ts
Normal file
34
ts_interfaces/data/service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { IServiceRessources } from './docker.js';
|
||||||
|
|
||||||
|
export interface IService {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
imageId: string;
|
||||||
|
imageVersion: string;
|
||||||
|
environment: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* the main secret bundle id, exclusive to the service
|
||||||
|
*/
|
||||||
|
secretBundleId: string;
|
||||||
|
/**
|
||||||
|
* those secret bundle ids do not belong to the service itself
|
||||||
|
* and thus live past the service lifecycle
|
||||||
|
*/
|
||||||
|
additionalSecretBundleIds?: string[];
|
||||||
|
scaleFactor: number;
|
||||||
|
balancingStrategy: 'round-robin' | 'least-connections';
|
||||||
|
ports: {
|
||||||
|
web: number;
|
||||||
|
custom?: { [domain: string]: string };
|
||||||
|
};
|
||||||
|
resources?: IServiceRessources;
|
||||||
|
domains: {
|
||||||
|
name: string;
|
||||||
|
port?: number;
|
||||||
|
protocol?: 'http' | 'https' | 'ssh';
|
||||||
|
}[];
|
||||||
|
deploymentIds: string[];
|
||||||
|
};
|
||||||
|
}
|
20
ts_interfaces/data/status.ts
Normal file
20
ts_interfaces/data/status.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IClusterStatus {
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
nodesCount: number;
|
||||||
|
containersUnderManagementCount: number;
|
||||||
|
nodeStatusId: string;
|
||||||
|
containerStatusArray: IContainerStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeStatus {
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContainerStatus {
|
||||||
|
serviceName: string;
|
||||||
|
dockerImageUrl: string;
|
||||||
|
dockerImageVersion: string;
|
||||||
|
}
|
5
ts_interfaces/data/traffic.ts
Normal file
5
ts_interfaces/data/traffic.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
interface IReverseProxyConfig extends plugins.tsclass.network.IReverseProxyConfig {}
|
||||||
|
|
||||||
|
export { type IReverseProxyConfig };
|
30
ts_interfaces/data/user.ts
Normal file
30
ts_interfaces/data/user.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export interface IToken {
|
||||||
|
token: string;
|
||||||
|
expiresAt: number;
|
||||||
|
assignedRoles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* an identity is assumed by authentication as a user
|
||||||
|
* an identity is ephemeral and has to be renewed regularly
|
||||||
|
*/
|
||||||
|
export interface IIdentity {
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
type: 'machine' | 'human';
|
||||||
|
role: 'admin' | 'user' | 'api' | 'cluster';
|
||||||
|
expiresAt: number;
|
||||||
|
/** the jwt token should contain above data for verification */
|
||||||
|
jwt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
type: 'machine' | 'human';
|
||||||
|
role: 'admin' | 'user' | 'api' | 'cluster';
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
tokens?: IToken[];
|
||||||
|
}
|
||||||
|
}
|
11
ts_interfaces/data/version.ts
Normal file
11
ts_interfaces/data/version.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface IContainerVersionData {
|
||||||
|
/**
|
||||||
|
* the docker image url
|
||||||
|
* example: registry.gitlab.com/hosttoday/ht-docker-node:latest
|
||||||
|
*/
|
||||||
|
dockerImageUrl: string;
|
||||||
|
/**
|
||||||
|
* the docker image version. Note: This is different from docker tags that are often used for versions.
|
||||||
|
*/
|
||||||
|
dockerImageVersion: string;
|
||||||
|
}
|
9
ts_interfaces/index.ts
Normal file
9
ts_interfaces/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as data from './data/index.js';
|
||||||
|
import * as platformservice from './platformservice/index.js';
|
||||||
|
import * as requests from './requests/index.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
data,
|
||||||
|
platformservice,
|
||||||
|
requests
|
||||||
|
}
|
1
ts_interfaces/platformservice/00readme.md
Normal file
1
ts_interfaces/platformservice/00readme.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
The platform folder contains types that can be used for talking with the underlying platform by apps running on serve.zone.
|
23
ts_interfaces/platformservice/aibridge.ts
Normal file
23
ts_interfaces/platformservice/aibridge.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IChat {
|
||||||
|
systemMessage: string;
|
||||||
|
messages: {
|
||||||
|
role: 'assistant' | 'user';
|
||||||
|
content: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Chat extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Chat
|
||||||
|
> {
|
||||||
|
method: 'chat',
|
||||||
|
request: {
|
||||||
|
chat: IChat;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
chat: IChat;
|
||||||
|
latestMessage: string;
|
||||||
|
}
|
||||||
|
}
|
13
ts_interfaces/platformservice/index.ts
Normal file
13
ts_interfaces/platformservice/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as aibridge from './aibridge.js';
|
||||||
|
import * as letter from './letter.js';
|
||||||
|
import * as mta from './mta.js';
|
||||||
|
import * as pushnotification from './pushnotification.js';
|
||||||
|
import * as sms from './sms.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
aibridge,
|
||||||
|
letter,
|
||||||
|
mta,
|
||||||
|
pushnotification,
|
||||||
|
sms,
|
||||||
|
}
|
34
ts_interfaces/platformservice/letter.ts
Normal file
34
ts_interfaces/platformservice/letter.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IRequest_SendLetter extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_SendLetter
|
||||||
|
> {
|
||||||
|
method: 'sendLetter';
|
||||||
|
request: {
|
||||||
|
/**
|
||||||
|
* will be used in logs
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* if you send any PDF / invoice that you have not made sure to be letterxpress compliant
|
||||||
|
* we strongly recommend using a cover page
|
||||||
|
*/
|
||||||
|
needsCover: boolean;
|
||||||
|
title?: string;
|
||||||
|
from?: plugins.tsclass.business.IAddress;
|
||||||
|
to?: plugins.tsclass.business.IAddress;
|
||||||
|
coverBody?: string;
|
||||||
|
service: ('Einschreiben')[];
|
||||||
|
pdfAttachments?: Array<{
|
||||||
|
name: string;
|
||||||
|
binaryAttachmentString: string;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
/**
|
||||||
|
* this process id allows status retrieval of the letter
|
||||||
|
*/
|
||||||
|
processId: string;
|
||||||
|
};
|
||||||
|
}
|
70
ts_interfaces/platformservice/mta.ts
Normal file
70
ts_interfaces/platformservice/mta.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export type TTemplates = 'default' | 'linkaction' | 'notification';
|
||||||
|
|
||||||
|
export interface IReq_SendEmail extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SendEmail
|
||||||
|
> {
|
||||||
|
method: 'sendEmail';
|
||||||
|
request: {
|
||||||
|
title: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
body: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
name: string;
|
||||||
|
binaryAttachmentString: string;
|
||||||
|
}>
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
/**
|
||||||
|
* the response id allows for handling of responses to that email
|
||||||
|
*/
|
||||||
|
responseId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_RegisterRecipient extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RegisterRecipient
|
||||||
|
> {
|
||||||
|
method: 'registerRecepient';
|
||||||
|
request: {
|
||||||
|
emailAddress: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
status: 'ok' | 'not ok';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_CheckEmailStatus extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CheckEmailStatus
|
||||||
|
> {
|
||||||
|
method: 'checkEmailStatus';
|
||||||
|
request: {
|
||||||
|
emailId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
status: string,
|
||||||
|
details?: { message: string; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetEMailStats extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetEMailStats
|
||||||
|
> {
|
||||||
|
method: 'getEmailStats';
|
||||||
|
request: {
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
totalEmailsSent: number;
|
||||||
|
totalEmailsDelivered: number;
|
||||||
|
totalEmailsBounced: number;
|
||||||
|
averageDeliveryTimeMs: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
};
|
||||||
|
}
|
16
ts_interfaces/platformservice/pushnotification.ts
Normal file
16
ts_interfaces/platformservice/pushnotification.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IRequest_SendPushNotification extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_SendPushNotification
|
||||||
|
> {
|
||||||
|
method: 'sendPushNotification';
|
||||||
|
request: {
|
||||||
|
deviceToken: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
response: {
|
||||||
|
ok: boolean;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
}
|
33
ts_interfaces/platformservice/sms.ts
Normal file
33
ts_interfaces/platformservice/sms.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IRequest_SendSms
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_SendSms
|
||||||
|
> {
|
||||||
|
method: 'sendSms';
|
||||||
|
request: {
|
||||||
|
toNumber: number;
|
||||||
|
fromName: string;
|
||||||
|
messageText: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
status: 'ok' | 'not ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_SendVerificationCode
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_SendVerificationCode
|
||||||
|
> {
|
||||||
|
method: 'sendVerificationCode';
|
||||||
|
request: {
|
||||||
|
toNumber: number;
|
||||||
|
fromName: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
status: 'ok' | 'not ok';
|
||||||
|
verificationCode: string;
|
||||||
|
}
|
||||||
|
}
|
20
ts_interfaces/plugins.ts
Normal file
20
ts_interfaces/plugins.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// @apiglobal scope
|
||||||
|
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
typedrequestInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// @push.rocks scope
|
||||||
|
import * as smartlogInterfaces from '@push.rocks/smartlog-interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
smartlogInterfaces,
|
||||||
|
}
|
||||||
|
|
||||||
|
// tsclass scope
|
||||||
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
|
||||||
|
export {
|
||||||
|
tsclass
|
||||||
|
}
|
391
ts_interfaces/readme.md
Normal file
391
ts_interfaces/readme.md
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
# @serve.zone/interfaces 📋
|
||||||
|
|
||||||
|
**TypeScript interfaces for the Cloudly ecosystem.** Type-safe contracts for multi-cloud infrastructure management.
|
||||||
|
|
||||||
|
## 🎯 What is @serve.zone/interfaces?
|
||||||
|
|
||||||
|
This package provides the complete set of TypeScript interfaces that power the Cloudly platform. It ensures type safety and consistency across all components - from API requests to data models, from service definitions to infrastructure configurations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🔒 Type Safety** - Comprehensive TypeScript interfaces for all Cloudly operations
|
||||||
|
- **📦 Modular Structure** - Organized by domain for easy navigation
|
||||||
|
- **🔄 Version Compatibility** - Interfaces versioned with the platform
|
||||||
|
- **📚 Well Documented** - Each interface includes JSDoc comments
|
||||||
|
- **🎭 Multi-Purpose** - Used by API clients, CLI tools, and web interfaces
|
||||||
|
- **✅ Validation Ready** - Compatible with runtime type checking libraries
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/interfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Interface Categories
|
||||||
|
|
||||||
|
### 📡 Request/Response Interfaces
|
||||||
|
|
||||||
|
Typed contracts for API communication:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IRequest_GetAllImages,
|
||||||
|
IRequest_CreateCluster,
|
||||||
|
IRequest_DeployService
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
// Type-safe request
|
||||||
|
const request: IRequest_GetAllImages['request'] = {
|
||||||
|
identity: userIdentity,
|
||||||
|
filters: {
|
||||||
|
tag: 'production'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type-safe response
|
||||||
|
const response: IRequest_GetAllImages['response'] = {
|
||||||
|
images: [...]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Data Models
|
||||||
|
|
||||||
|
Core data structures for Cloudly entities:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
ICluster,
|
||||||
|
IService,
|
||||||
|
IImage,
|
||||||
|
ISecret,
|
||||||
|
IServer
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
// Define a service
|
||||||
|
const service: IService = {
|
||||||
|
id: 'service-123',
|
||||||
|
data: {
|
||||||
|
name: 'api-service',
|
||||||
|
imageId: 'image-456',
|
||||||
|
imageVersion: '2.0.0',
|
||||||
|
environment: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
},
|
||||||
|
scaleFactor: 3,
|
||||||
|
balancingStrategy: 'round-robin',
|
||||||
|
ports: {
|
||||||
|
web: 80,
|
||||||
|
metrics: 9090
|
||||||
|
},
|
||||||
|
domains: [
|
||||||
|
{ name: 'api.example.com' }
|
||||||
|
],
|
||||||
|
deploymentIds: [],
|
||||||
|
deploymentDirectiveIds: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Authentication & Identity
|
||||||
|
|
||||||
|
Identity management interfaces:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IIdentity,
|
||||||
|
IServiceToken,
|
||||||
|
IPermission
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const identity: IIdentity = {
|
||||||
|
id: 'user-789',
|
||||||
|
name: 'service-account',
|
||||||
|
type: 'service',
|
||||||
|
permissions: ['cluster:read', 'service:write'],
|
||||||
|
tokenHash: 'hashed-token',
|
||||||
|
metadata: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
lastAccess: new Date()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 Network Configuration
|
||||||
|
|
||||||
|
Networking and routing interfaces:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IReverseProxyConfig,
|
||||||
|
IDomainConfig,
|
||||||
|
ILoadBalancerConfig
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const proxyConfig: IReverseProxyConfig = {
|
||||||
|
domain: 'app.example.com',
|
||||||
|
path: '/api',
|
||||||
|
serviceAddress: 'http://api-service:3000',
|
||||||
|
ssl: true,
|
||||||
|
headers: {
|
||||||
|
'X-Real-IP': '$remote_addr'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Monitoring & Metrics
|
||||||
|
|
||||||
|
Observability interfaces:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IServerMetrics,
|
||||||
|
IServiceMetrics,
|
||||||
|
IClusterHealth
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const metrics: IServerMetrics = {
|
||||||
|
serverId: 'server-001',
|
||||||
|
cpuUsageInPercent: 65,
|
||||||
|
memoryUsageinMB: 3072,
|
||||||
|
memoryAvailableInMB: 8192,
|
||||||
|
diskUsageInPercent: 40,
|
||||||
|
networkInMbps: 100,
|
||||||
|
networkOutMbps: 150,
|
||||||
|
containerCount: 12,
|
||||||
|
containerMetrics: [...]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Secret Management
|
||||||
|
|
||||||
|
Security and credential interfaces:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
ISecretGroup,
|
||||||
|
ISecretBundle,
|
||||||
|
IEncryptedData
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const secretGroup: ISecretGroup = {
|
||||||
|
id: 'secrets-123',
|
||||||
|
name: 'database-credentials',
|
||||||
|
secrets: [
|
||||||
|
{
|
||||||
|
key: 'DB_HOST',
|
||||||
|
value: 'encrypted-value',
|
||||||
|
encrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
value: 'encrypted-value',
|
||||||
|
encrypted: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
environment: 'production',
|
||||||
|
service: 'api'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Common Usage Patterns
|
||||||
|
|
||||||
|
### Creating Type-Safe API Clients
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IRequest_CreateService,
|
||||||
|
IService
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
class ServiceClient {
|
||||||
|
async createService(
|
||||||
|
serviceData: IService['data']
|
||||||
|
): Promise<IService> {
|
||||||
|
const request: IRequest_CreateService['request'] = {
|
||||||
|
identity: this.identity,
|
||||||
|
serviceData
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.send<IRequest_CreateService>(
|
||||||
|
'createService',
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating Incoming Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ICluster } from '@serve.zone/interfaces';
|
||||||
|
import { validateType } from 'your-validation-library';
|
||||||
|
|
||||||
|
function validateClusterData(data: unknown): ICluster {
|
||||||
|
if (!validateType<ICluster>(data)) {
|
||||||
|
throw new Error('Invalid cluster data');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Configuration Objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
ICloudlyConfig,
|
||||||
|
IMongoDescriptor
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const config: ICloudlyConfig = {
|
||||||
|
cfToken: process.env.CF_TOKEN!,
|
||||||
|
hetznerToken: process.env.HETZNER_TOKEN!,
|
||||||
|
environment: 'production',
|
||||||
|
letsEncryptEmail: 'certs@example.com',
|
||||||
|
publicUrl: 'cloudly.example.com',
|
||||||
|
publicPort: 443,
|
||||||
|
mongoDescriptor: {
|
||||||
|
mongoDbUrl: process.env.MONGO_URL!,
|
||||||
|
mongoDbName: 'cloudly',
|
||||||
|
mongoDbUser: process.env.MONGO_USER!,
|
||||||
|
mongoDbPass: process.env.MONGO_PASS!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Features
|
||||||
|
|
||||||
|
### Generic Request Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ITypedRequest } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
class RequestHandler {
|
||||||
|
async handle<T extends ITypedRequest>(
|
||||||
|
request: T['request']
|
||||||
|
): Promise<T['response']> {
|
||||||
|
// Type-safe request handling
|
||||||
|
return this.processRequest(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discriminated Unions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IDeploymentStatus } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
function handleStatus(status: IDeploymentStatus) {
|
||||||
|
switch (status.type) {
|
||||||
|
case 'pending':
|
||||||
|
console.log('Deployment pending...');
|
||||||
|
break;
|
||||||
|
case 'running':
|
||||||
|
console.log(`Running on ${status.serverId}`);
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
console.log(`Failed: ${status.error}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extending Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IService } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
interface IExtendedService extends IService {
|
||||||
|
customMetadata: {
|
||||||
|
team: string;
|
||||||
|
costCenter: string;
|
||||||
|
sla: 'standard' | 'premium';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Version Compatibility
|
||||||
|
|
||||||
|
| @serve.zone/interfaces | @serve.zone/cloudly | @serve.zone/api | @serve.zone/cli |
|
||||||
|
|------------------------|---------------------|-----------------|-----------------|
|
||||||
|
| 5.x | 5.x | 5.x | 5.x |
|
||||||
|
| 4.x | 4.x | 4.x | 4.x |
|
||||||
|
| 3.x | 3.x | 3.x | 3.x |
|
||||||
|
|
||||||
|
## 📖 Interface Documentation
|
||||||
|
|
||||||
|
All interfaces include comprehensive JSDoc comments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Represents a cluster configuration
|
||||||
|
* @interface ICluster
|
||||||
|
*/
|
||||||
|
export interface ICluster {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the cluster
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cluster configuration data
|
||||||
|
* @type {IClusterData}
|
||||||
|
*/
|
||||||
|
data: IClusterData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development Tips
|
||||||
|
|
||||||
|
### Import Organization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Group imports by category
|
||||||
|
import {
|
||||||
|
// Data models
|
||||||
|
ICluster,
|
||||||
|
IService,
|
||||||
|
IImage,
|
||||||
|
|
||||||
|
// Requests
|
||||||
|
IRequest_CreateCluster,
|
||||||
|
IRequest_DeployService,
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
ICloudlyConfig,
|
||||||
|
IReverseProxyConfig
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IService, ICluster } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
function isService(entity: IService | ICluster): entity is IService {
|
||||||
|
return 'imageId' in entity.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user