Compare commits

...

9 Commits

Author SHA1 Message Date
cdbab26008 1.1.4
Some checks failed
Docker (tags) / security (push) Failing after 19s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2024-06-13 10:07:54 +02:00
1983c64b77 fix(core): update 2024-06-13 10:07:53 +02:00
a6e3a7f5fe prepare service management 2024-06-13 09:36:02 +02:00
6dd687012f 1.1.3
Some checks failed
Docker (tags) / security (push) Failing after 15s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2024-06-05 14:13:04 +02:00
55b2872ffc fix(structure): improve structure, prepare better CI integration 2024-06-05 14:13:03 +02:00
2e6e7f6ca8 1.1.2
Some checks failed
Docker (tags) / security (push) Failing after 14s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2024-06-02 21:39:32 +02:00
f453ce3126 fix(imagemanager): prepare proper storage and retrieval of container images 2024-06-02 21:39:31 +02:00
b8dd84b8a6 1.1.1
Some checks failed
Docker (tags) / security (push) Failing after 14s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2024-06-01 05:48:57 +02:00
338ed5ed75 fix(image registry): start work on image registry 2024-06-01 05:48:57 +02:00
41 changed files with 1656 additions and 1053 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/cloudly", "name": "@serve.zone/cloudly",
"version": "1.1.0", "version": "1.1.4",
"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 cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.",
"type": "module", "type": "module",
@ -26,14 +26,14 @@
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.90",
"@git.zone/tswatch": "^2.0.23", "@git.zone/tswatch": "^2.0.23",
"@push.rocks/tapbundle": "^5.0.23", "@push.rocks/tapbundle": "^5.0.23",
"@types/node": "^20.12.13" "@types/node": "^20.14.2"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "3.0.28", "@api.global/typedrequest": "3.0.30",
"@api.global/typedserver": "^3.0.50", "@api.global/typedserver": "^3.0.50",
"@api.global/typedsocket": "^3.0.1", "@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.0.1", "@apiclient.xyz/cloudflare": "^6.0.1",
"@apiclient.xyz/digitalocean": "^1.0.5", "@apiclient.xyz/docker": "^1.2.2",
"@apiclient.xyz/hetznercloud": "^1.0.18", "@apiclient.xyz/hetznercloud": "^1.0.18",
"@apiclient.xyz/slack": "^3.0.9", "@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.0.289", "@design.estate/dees-catalog": "^1.0.289",
@ -41,32 +41,34 @@
"@design.estate/dees-element": "^2.0.34", "@design.estate/dees-element": "^2.0.34",
"@git.zone/tsrun": "^1.2.37", "@git.zone/tsrun": "^1.2.37",
"@push.rocks/early": "^4.0.3", "@push.rocks/early": "^4.0.3",
"@push.rocks/npmextra": "^5.0.13", "@push.rocks/npmextra": "^5.0.17",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.0.5", "@push.rocks/qenv": "^6.0.5",
"@push.rocks/smartacme": "^4.0.8", "@push.rocks/smartacme": "^4.0.8",
"@push.rocks/smartbucket": "^3.0.9", "@push.rocks/smartbucket": "^3.0.15",
"@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdata": "^5.2.1", "@push.rocks/smartdata": "^5.2.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/smartfile": "^11.0.20",
"@push.rocks/smartguard": "^3.0.2", "@push.rocks/smartguard": "^3.0.2",
"@push.rocks/smartjson": "^5.0.19", "@push.rocks/smartjson": "^5.0.19",
"@push.rocks/smartjwt": "^2.0.4", "@push.rocks/smartjwt": "^2.0.4",
"@push.rocks/smartlog": "^3.0.6", "@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.11", "@push.rocks/smartlog-destination-clickhouse": "^1.0.11",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartrequest": "^2.0.22", "@push.rocks/smartrequest": "^2.0.22",
"@push.rocks/smartrx": "^3.0.7", "@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartssh": "^2.0.1", "@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstate": "^2.0.17",
"@push.rocks/smartstream": "^3.0.44",
"@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", "@serve.zone/interfaces": "^1.0.72",
"@tsclass/tsclass": "^4.0.54" "@tsclass/tsclass": "^4.0.55"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

1900
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

24
test/test.apiclient.ts Normal file
View File

@ -0,0 +1,24 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as cloudlyApiClient from '../ts_apiclient/index.js';
let testClient: cloudlyApiClient.CloudlyApiClient;
tap.test('should create a new cloudlyApiClient', async () => {
testClient = new cloudlyApiClient.CloudlyApiClient({
registerAs: 'api',
cloudlyUrl: 'http://localhost:3000',
});
await testClient.start();
expect(testClient).toBeTruthy();
});
tap.test('should trigger a server action', async () => {
})
tap.test('should stop the apiclient', async () => {
await testClient.stop();
})
export default tap.start();

View File

@ -10,20 +10,19 @@ 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 = { const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: testQenv.getEnvVarOnDemand('CF_TOKEN'), cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration', environment: 'integration',
letsEncryptEmail: testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'), letsEncryptEmail: await testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: testQenv.getEnvVarOnDemand('SERVEZONE_URL'), publicUrl: await testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: testQenv.getEnvVarOnDemand('SERVEZONE_PORT'), publicPort: await testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
mongoDescriptor: { mongoDescriptor: {
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'), mongoDbName: await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbUser: testQenv.getEnvVarOnDemand('MONGODB_USER'), mongoDbUser: await testQenv.getEnvVarOnDemand('MONGODB_USER'),
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'), mongoDbPass: await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL'), mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL'),
}, },
digitalOceanToken: testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN'),
}; };
testCloudly = new cloudly.Cloudly(cloudlyConfig); testCloudly = new cloudly.Cloudly();
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly); expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
}); });

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '1.1.0', version: '1.1.4',
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 cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.'
} }

View 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: [],
}
}
];

View File

@ -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}`,
includedImages: [],
type: 'external',
description: 'Demo Purpose', description: 'Demo Purpose',
includedSecretGroupIds: [secretGroup.id], includedSecretGroupIds: [secretGroup.id],
includedTags: secretGroup.data.tags, includedTags: secretGroup.data.tags,

View File

@ -0,0 +1,19 @@
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: {
username: envAdminUser.split(':')[0],
password: envAdminUser.split(':')[1],
role: 'admin',
},
});
}
return users;
};

View File

@ -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();
}
} }

View File

@ -15,10 +15,10 @@ 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 { 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';

View File

@ -11,9 +11,6 @@ export class CloudlyConfig {
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;
@ -43,6 +40,7 @@ export class CloudlyConfig {
useSsl: true, useSsl: true,
}, },
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',

View File

@ -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, ? {
publicKey: sslCert.publicKey, privateKey: sslCert.privateKey,
} : {}), 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();
} }

View File

@ -34,7 +34,10 @@ export class LetsencryptConnector {
}, },
mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor, mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor,
}); });
await this.smartacme.init(); await this.smartacme.init().catch(err => {
console.error('error in init', err);
console.log(`trying again in a few minutes`)
});
} }
/** /**

View File

@ -1,9 +0,0 @@
export const users = [
{
id: 'user1',
data: {
username: 'admin',
password: 'password',
}
}
]

View File

@ -21,8 +21,10 @@ const runCli = async () => {
); );
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 }

View File

@ -30,6 +30,7 @@ export class CloudlyAuthManager {
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('jwtKeys');
@ -51,7 +52,7 @@ export class CloudlyAuthManager {
if (!user) { if (!user) {
logger.log('warn', 'login failed'); logger.log('warn', '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',
}); });
@ -69,8 +70,12 @@ export class CloudlyAuthManager {
public adminJwtGuard = new plugins.smartguard.Guard<{jwt: string}>(async (dataArg) => { public adminJwtGuard = new plugins.smartguard.Guard<{jwt: string}>(async (dataArg) => {
const jwt = dataArg.jwt; const jwt = dataArg.jwt;
const jwtData: IJwtData = await this.cloudlyRef.config.smartjwtInstance.verifyJWTAndGetData(jwt); const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
const user = await this.CUser.getInstance({id: jwtData.userId}); const user = await this.CUser.getInstance({id: jwtData.userId});
return user.data.role === 'admin'; const isAdminBool = user.data.role === 'admin';
console.log(`user is admin: ${isAdminBool}`);
return isAdminBool;
}, {
failedHint: 'user is not admin.'
}) })
} }

View File

@ -1,13 +1,16 @@
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
> {
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,
} },
}); });
} }
@ -20,5 +23,5 @@ export class User extends plugins.smartdata.SmartDataDbDoc<User, User> {
role: 'admin' | 'user'; role: 'admin' | 'user';
username: string; username: string;
password: string; password: string;
} };
} }

View File

@ -3,7 +3,7 @@ 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';
export class ClusterManager { export class ClusterManager {
public ready = plugins.smartpromise.defer(); public ready = plugins.smartpromise.defer();
@ -28,7 +28,6 @@ export class ClusterManager {
name: dataArg.clusterName, name: dataArg.clusterName,
jumpCode: plugins.smartunique.uniSimple('cluster'), jumpCode: plugins.smartunique.uniSimple('cluster'),
jumpCodeUsedAt: null, 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: [],
@ -54,6 +53,17 @@ export class ClusterManager {
}; };
}) })
); );
// delete cluster
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_DeleteCluster>(
new plugins.typedrequest.TypedHandler('deleteCluster', async (reqDataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], reqDataArg);
await this.deleteCluster(reqDataArg.clusterId);
return {
success: true,
};
})
);
} }
public async init() { public async init() {
@ -86,9 +96,9 @@ export class ClusterManager {
await this.ready.promise; await this.ready.promise;
return await Cluster.getInstance({ return await Cluster.getInstance({
id: clusterIdentifier.clusterId,
data: { data: {
name: clusterIdentifier.clusterName, name: clusterIdentifier.clusterName,
secretKey: clusterIdentifier.secretKey,
}, },
}); });
} }
@ -128,4 +138,10 @@ export class ClusterManager {
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();
}
} }

View File

@ -26,8 +26,12 @@ export class CloudlyCoreflowManager {
return { return {
clusterIdentifier: { clusterIdentifier: {
clusterId: clusterConfig.id,
clusterName: clusterConfig.data.name, clusterName: clusterConfig.data.name,
secretKey: clusterConfig.data.secretKey, jwt: await this.cloudlyRef.authManager.smartjwtInstance.createJWT({
status: 'loggedIn',
userId: 'cluster:' + clusterConfig.id, // TODO: create real users for clusters
})
}, },
}; };
}) })
@ -47,7 +51,8 @@ export class CloudlyCoreflowManager {
); );
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 clusterConfigSet.createSavableObject(),
deploymentDirectives: [],
}; };
} }
) )

View File

@ -1,12 +1,20 @@
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<Image, plugins.servezoneInterfaces.data.IImage, ImageManager> {
public static async create(imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>) { 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 +26,12 @@ 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('/', '__')
}
} }

View File

@ -5,19 +5,54 @@ 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.adminJwtGuard], reqArg);
const image = await this.CImage.create({
name: reqArg.name,
description: reqArg.description,
versions: [],
});
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.adminJwtGuard], 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',
@ -36,39 +71,35 @@ export class ImageManager {
); );
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({ const image = await this.CImage.getInstance({
name: reqArg.name, id: reqArg.imageId,
}); });
if (!image) {
throw new plugins.typedrequest.TypedResponseError('Image not found');
}
const imageVersion = reqArg.versionString;
const imagePushStream = reqArg.imageStream;
return { return {
image: await image.createSavableObject(), allowed: true,
}; };
} }
) )
); );
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PushImage>( new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImageVersion>(
'pushImage', 'pullImageVersion',
async (reqArg) => {
const pushStream = reqArg.imageStream;
return {};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImage>(
'pullImage',
async (reqArg) => { async (reqArg) => {
const image = await this.CImage.getInstance({ const image = await this.CImage.getInstance({
data: { id: reqArg.imageId,
name: reqArg.name,
},
}); });
const imageVersion = null; 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,
@ -87,11 +118,9 @@ export class ImageManager {
); );
const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test'); const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test');
await bucket.fastPut({ path: 'test/test.txt', contents: 'hello' }); await bucket.fastPut({ path: 'test/test.txt', contents: 'hello' });
}
public async createImage(nameArg: string) { this.imageDir = await bucket.getDirectoryFromPath({
const newImage = await this.CImage.create({ path: 'images',
name: nameArg,
}); });
} }
} }

View File

@ -38,7 +38,8 @@ export class CloudlySecretManager {
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>( new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(
'adminGetConfigBundlesAndSecretGroups', 'adminGetConfigBundlesAndSecretGroups',
async (dataArg) => { async (dataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], dataArg);
dataArg.jwt dataArg.jwt
const secretBundles = await SecretBundle.getInstances({}); const secretBundles = await SecretBundle.getInstances({});
const secretGroups = await SecretGroup.getInstances({}); const secretGroups = await SecretGroup.getInstances({});

View File

@ -0,0 +1,84 @@
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 make sure we use the correct npm registry
bash -c "npm config set registry ${this.optionsArg.npmRegistry}"
# lets install spark
bash -c "npm 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;
};
}

View File

@ -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;
@ -63,7 +65,8 @@ 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}`);

View File

@ -0,0 +1,12 @@
import * as plugins from '../plugins.js';
export class Service extends plugins.smartdata.SmartDataDbDoc<Service, plugins.servezoneInterfaces.data.IService, ServiceManager> {
@plugins.smartdata.svDb()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IService['data'];
}

View File

@ -0,0 +1,18 @@
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;
}
}

View File

@ -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';

View File

@ -1,8 +1,10 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
export type TClientType = 'coreflow' | 'cli' | 'serverconfig'; export type TClientType = 'api' | 'ci' | 'coreflow' | 'cli' | 'serverconfig';
export class CloudlyClient { import { Image } from './classes.image.js';
export class CloudlyApiClient {
private cloudlyUrl: string; private cloudlyUrl: string;
private registerAs: string; private registerAs: string;
@ -18,9 +20,13 @@ export class CloudlyClient {
plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction['request'] plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction['request']
>(); >();
constructor(registerAsArg: TClientType) { constructor(optionsArg?: {
this.cloudlyUrl = process.env.CLOUDLY_URL || 'https://cloudly.layer.io:443'; registerAs: TClientType;
this.registerAs = registerAsArg; cloudlyUrl?: string;
}) {
this.registerAs = optionsArg.registerAs;
this.cloudlyUrl =
optionsArg?.cloudlyUrl || process.env.CLOUDLY_URL || 'https://cloudly.layer.io:443';
console.log( console.log(
`creating LoleCloudlyClient: registering as ${this.registerAs} and target url ${this.cloudlyUrl}` `creating LoleCloudlyClient: registering as ${this.registerAs} and target url ${this.cloudlyUrl}`
@ -33,13 +39,6 @@ export class CloudlyClient {
}) })
); );
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>( this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.server.IRequest_TriggerServerAction>(
new plugins.typedrequest.TypedHandler('triggerServerAction', async (dataArg) => { new plugins.typedrequest.TypedHandler('triggerServerAction', async (dataArg) => {
this.serverActionSubject.next(dataArg); this.serverActionSubject.next(dataArg);
@ -55,15 +54,20 @@ export class CloudlyClient {
this.typedrouter, this.typedrouter,
this.cloudlyUrl this.cloudlyUrl
); );
console.log(
`CloudlyCluent connected to cloudly at ${this.cloudlyUrl}. Remember to get an identity.`
);
} }
public async stop() { public async stop() {
await this.typedsocketClient.stop(); await this.typedsocketClient.stop();
} }
public identity: plugins.servezoneInterfaces.data.IClusterIdentifier;
public async getIdentityByJumpCode( public async getIdentityByJumpCode(
jumpCodeArg: string, jumpCodeArg: string,
tagConnection = false tagConnection = false,
statefullIdentity = true
): Promise<plugins.servezoneInterfaces.data.IClusterIdentifier> { ): Promise<plugins.servezoneInterfaces.data.IClusterIdentifier> {
const identityRequest = const identityRequest =
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>( this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>(
@ -80,6 +84,10 @@ export class CloudlyClient {
this.typedsocketClient.addTag('identity', identity); this.typedsocketClient.addTag('identity', identity);
} }
if (statefullIdentity) {
this.identity = identity;
}
return identity; return identity;
} }
@ -91,7 +99,7 @@ export class CloudlyClient {
'getClusterConfig' 'getClusterConfig'
); );
const response = await clusterConfigRequest.fire({ const response = await clusterConfigRequest.fire({
jwt: '', // TODO: do proper auth here jwt: '',
clusterIdentifier: identityArg, clusterIdentifier: identityArg,
}); });
return response.configData; return response.configData;
@ -106,7 +114,7 @@ export class CloudlyClient {
); );
const response = await serverConfigRequest.fire({ const response = await serverConfigRequest.fire({
jwt: '', // TODO: do proper auth here jwt: '', // TODO: do proper auth here
serverId: '' // TODO: get server id here serverId: '', // TODO: get server id here
}); });
return response.configData; return response.configData;
} }
@ -116,7 +124,9 @@ export class CloudlyClient {
* @param serviceNameArg * @param serviceNameArg
* @param domainNameArg * @param domainNameArg
*/ */
public async getCertificateForDomainOverHttps(domainNameArg: string): Promise<plugins.tsclass.network.ICert> { public async getCertificateForDomainOverHttps(
domainNameArg: string
): Promise<plugins.tsclass.network.ICert> {
const typedCertificateRequest = const typedCertificateRequest =
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetSslCertificate>( this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.certificate.IRequest_Any_Cloudly_GetSslCertificate>(
'getSslCertificate' 'getSslCertificate'
@ -127,4 +137,9 @@ export class CloudlyClient {
}); });
return typedResponse.certificate; return typedResponse.certificate;
} }
// Images
public async getImages() {
return Image.getImages(this);
}
} }

View File

@ -1,5 +1,84 @@
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({
jwt: cloudlyClientRef.identity.jwt,
});
const resultImages: Image[] = [];
for (const image of response.images) {
const newImage = new Image(cloudlyClientRef);
Object.assign(newImage, image);
resultImages.push(newImage);
}
return resultImages;
}
// 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_GetImageMetadata>(
'getImageMetadata'
);
const response = await getVersionsTR.fire({
jwt: this.cloudlyClientRef.identity.jwt,
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 pullImageTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.image.IRequest_PushImageVersion>(
'pushImageVersion'
);
const virtualStream = new plugins.typedrequest.VirtualStream();
const response = await pullImageTR.fire({
jwt: this.cloudlyClientRef.identity.jwt,
imageId: this.id,
versionString: '',
imageStream: virtualStream,
});
await virtualStream.readFromWebstream(imageReadableArg);
await done.promise;
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({
jwt: this.cloudlyClientRef.identity.jwt,
imageId: this.id,
versionString: versionStringArg,
});
const imageStream = response.imageStream;
const webduplexStream = new plugins.webstream.WebDuplexStream({});
imageStream.writeToWebstream(webduplexStream.writable);
return webduplexStream.readable;
};
} }

View File

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

View File

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

View File

@ -6,10 +6,14 @@ export {
} }
// @push.rocks scope // @push.rocks scope
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 {
smartpromise,
smartrx, smartrx,
webstream,
} }
// @api.global scope // @api.global scope

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '1.1.0', version: '1.1.4',
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 cloud manager leveraging Docker Swarmkit for multi-cloud operations including DigitalOcean, Hetzner Cloud, and Cloudflare, with integration support and robust configuration management system.'
} }

View File

@ -5,7 +5,7 @@ const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate();
export interface ILoginState { export interface ILoginState {
jwt: string; jwt: string;
} }
export const loginStatePart = await appstate.getStatePart<ILoginState>( export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState> = await appstate.getStatePart<ILoginState>(
'login', 'login',
{ jwt: null }, { jwt: null },
'persistent' 'persistent'
@ -72,7 +72,7 @@ export const dataState = await appstate.getStatePart<IDataState>(
); );
// Getting data // Getting data
export const getDataAction = dataState.createAction(async (statePartArg) => { export const getAllDataAction = dataState.createAction(async (statePartArg, partialArg?: 'secrets' | 'images') => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
// Secrets // Secrets
const trGetSecrets = const trGetSecrets =
@ -88,6 +88,20 @@ export const getDataAction = dataState.createAction(async (statePartArg) => {
...response, ...response,
}; };
// images
const trGetImages =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_GetAllImages>(
'/typedrequest',
'getAllImages'
);
const responseImages = await trGetImages.fire({
jwt: loginStatePart.getState().jwt,
});
currentState = {
...currentState,
...responseImages,
};
// Clusters // Clusters
const trGetClusters = const trGetClusters =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_GetAllClusters>( new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_GetAllClusters>(
@ -120,7 +134,7 @@ export const createSecretGroupAction = dataState.createAction(
secretBundles: [], secretBundles: [],
secretGroups: [payloadArg], secretGroups: [payloadArg],
}); });
currentState = await dataState.dispatchAction(getDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
return currentState; return currentState;
} }
@ -139,7 +153,7 @@ export const deleteSecretGroupAction = dataState.createAction(
secretBundleIds: [], secretBundleIds: [],
secretGroupIds: [payloadArg.secretGroupId], secretGroupIds: [payloadArg.secretGroupId],
}); });
currentState = await dataState.dispatchAction(getDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
); );
@ -158,7 +172,53 @@ export const deleteSecretBundleAction = dataState.createAction(
secretBundleIds: [payloadArg.configBundleId], secretBundleIds: [payloadArg.configBundleId],
secretGroupIds: [], secretGroupIds: [],
}); });
currentState = await dataState.dispatchAction(getDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
// image actions
export const createImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageName: string, description: string }) => {
let currentState = statePartArg.getState();
const trCreateImage =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_CreateImage>(
'/typedrequest',
'createImage'
);
const response = await trCreateImage.fire({
jwt: loginStatePart.getState().jwt,
name: payloadArg.imageName,
description: payloadArg.description,
});
currentState = {
...currentState,
...{
images: [...currentState.images, response.image],
},
};
return currentState;
}
);
export const deleteImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageId: string }) => {
let currentState = statePartArg.getState();
const trDeleteImage =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_DeleteImage>(
'/typedrequest',
'deleteImage'
);
const response = await trDeleteImage.fire({
jwt: loginStatePart.getState().jwt,
imageId: payloadArg.imageId,
});
currentState = {
...currentState,
...{
images: currentState.images.filter((image) => image.id !== payloadArg.imageId),
},
};
return currentState; return currentState;
} }
); );

View File

@ -42,6 +42,7 @@ export class CloudlyDashboard extends DeesElement {
constructor() { constructor() {
super(); super();
document.title = `cloudly v${commitinfo.version}`;
const subcription = appstate.dataState const subcription = appstate.dataState
.select((stateArg) => stateArg) .select((stateArg) => stateArg)
.subscribe((dataArg) => { .subscribe((dataArg) => {
@ -148,7 +149,7 @@ export class CloudlyDashboard extends DeesElement {
action: async () => { action: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'About', heading: 'About',
content: html`configvault ${commitinfo.version}`, content: html`cloudly ${commitinfo.version}`,
menuOptions: [ menuOptions: [
{ {
name: 'close', name: 'close',
@ -171,7 +172,7 @@ export class CloudlyDashboard extends DeesElement {
if (loginState.jwt) { if (loginState.jwt) {
this.jwt = loginState.jwt; this.jwt = loginState.jwt;
await simpleLogin.switchToSlottedContent(); await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getDataAction, null); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
} }
} }
@ -190,7 +191,7 @@ export class CloudlyDashboard extends DeesElement {
this.jwt = state.jwt; this.jwt = state.jwt;
form.setStatus('success', 'Logged in!'); form.setStatus('success', 'Logged in!');
await simpleLogin.switchToSlottedContent(); await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getDataAction, null); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
} else { } else {
form.setStatus('error', 'Login failed!'); form.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000); await domtools.convenience.smartdelay.delayFor(2000);

View File

@ -37,35 +37,25 @@ export class CloudlyViewImages extends DeesElement {
return html` return html`
<cloudly-sectionheading>Images</cloudly-sectionheading> <cloudly-sectionheading>Images</cloudly-sectionheading>
<dees-table <dees-table
heading1="SecretGroups" heading1="Images"
heading2="decoded in client" heading2="an image is needed for running a service"
.data=${this.data.images} .data=${this.data.images}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => { .displayFunction=${(image: plugins.interfaces.data.IImage) => {
return { return {
name: secretGroup.data.name, id: image.id,
priority: secretGroup.data.priority, name: image.data.name,
tags: html`<dees-chips description: image.data.description,
.selectionMode=${'none'} versions: image.data.versions.length,
.selectableChips=${secretGroup.data.tags}
></dees-chips>`,
key: secretGroup.data.key,
history: (() => {
const allHistory = [];
for (const environment in secretGroup.data.environments) {
allHistory.push(...secretGroup.data.environments[environment].history);
}
return allHistory.length;
})(),
}; };
}} }}
.dataActions=${[ .dataActions=${[
{ {
name: 'add SecretGroup', name: 'create Image',
type: ['header', 'footer'], type: ['header', 'footer'],
iconName: 'plus', iconName: 'plus',
actionFunc: async () => { actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({ plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new SecretGroup', heading: 'create new Image',
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text <dees-input-text
@ -78,50 +68,6 @@ export class CloudlyViewImages extends DeesElement {
.key=${'data.description'} .key=${'data.description'}
.value=${''} .value=${''}
></dees-input-text> ></dees-input-text>
<dees-input-text
.label=${'Secret Key (data.key)'}
.key=${'data.key'}
.value=${''}
></dees-input-text>
<dees-table
.heading1=${'Environments'}
.heading2=${'keys need to be unique'}
key="environments"
.data=${[
{
environment: 'production',
value: '',
},
{
environment: 'staging',
value: '',
},
]}
.dataActions=${[
{
name: 'add environment',
iconName: 'plus',
type: ['footer'],
actionFunc: async (dataArg) => {
dataArg.table.data.push({
environment: 'new environment',
value: '',
});
dataArg.table.requestUpdate('data');
},
},
{
name: 'delete environment',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (dataArg) => {
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
dataArg.table.requestUpdate('data');
},
},
] as plugins.deesCatalog.ITableAction[]}
.editableFields=${['environment', 'value']}
></dees-table>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [
@ -138,24 +84,9 @@ export class CloudlyViewImages extends DeesElement {
const formData = await deesForm.collectFormData(); const formData = await deesForm.collectFormData();
console.log(`Prepare saving of data:`); console.log(`Prepare saving of data:`);
console.log(formData); console.log(formData);
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = await appstate.dataState.dispatchAction(appstate.createImageAction, {
{}; imageName: formData['data.name'] as string,
for (const itemArg of formData['environments'] as any[]) { description: formData['data.description'] as string,
environments[itemArg.environment] = {
value: itemArg.value,
history: [],
lastUpdated: Date.now(),
};
}
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
id: null,
data: {
name: formData['data.name'] as string,
description: formData['data.description'] as string,
key: formData['data.key'] as string,
environments,
tags: [],
},
}); });
await modalArg.destroy(); await modalArg.destroy();
}, },
@ -327,16 +258,16 @@ export class CloudlyViewImages extends DeesElement {
iconName: 'trash', iconName: 'trash',
type: ['contextmenu', 'inRow'], type: ['contextmenu', 'inRow'],
actionFunc: async ( actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup> itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>
) => { ) => {
plugins.deesCatalog.DeesModal.createAndShow({ plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ${itemArg.item.data.key}`, heading: `Delete Image "${itemArg.item.data.name}"`,
content: html` content: html`
<div style="text-align:center">Do you really want to delete the secret?</div> <div style="text-align:center">Do you really want to delete the image?</div>
<div <div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;" style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
> >
${itemArg.item.data.key} ${itemArg.item.id}
</div> </div>
`, `,
menuOptions: [ menuOptions: [
@ -350,8 +281,8 @@ export class CloudlyViewImages extends DeesElement {
name: 'delete', name: 'delete',
action: async (modalArg) => { action: async (modalArg) => {
console.log(`Delete ${itemArg.item.id}`); console.log(`Delete ${itemArg.item.id}`);
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { await appstate.dataState.dispatchAction(appstate.deleteImageAction, {
secretGroupId: itemArg.item.id, imageId: itemArg.item.id,
}); });
await modalArg.destroy(); await modalArg.destroy();
}, },

View File

@ -21,12 +21,12 @@ export class CloudlyViewSecretBundles extends DeesElement {
constructor() { constructor() {
super(); super();
const subecription = appstate.dataState const subscription = appstate.dataState
.select((stateArg) => stateArg) .select((stateArg) => stateArg)
.subscribe((dataArg) => { .subscribe((dataArg) => {
this.data = dataArg; this.data = dataArg;
}); });
this.rxSubscriptions.push(subecription); this.rxSubscriptions.push(subscription);
} }
public static styles = [ public static styles = [
@ -144,7 +144,7 @@ export class CloudlyViewSecretBundles extends DeesElement {
}, },
{ {
name: 'edit', name: 'edit',
iconName: 'edit', iconName: 'penToSquare',
type: ['doubleClick', 'contextmenu', 'inRow'], type: ['doubleClick', 'contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => { actionFunc: async (actionDataArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ const modal = await plugins.deesCatalog.DeesModal.createAndShow({

View File

@ -13,7 +13,9 @@ export { deesDomtools, deesElement, deesCatalog };
// @push.rocks scope // @push.rocks scope
import * as webjwt from '@push.rocks/webjwt'; import * as webjwt from '@push.rocks/webjwt';
import * as smartstate from '@push.rocks/smartstate';
export { export {
webjwt, webjwt,
smartstate,
} }