Compare commits

..

6 Commits

Author SHA1 Message Date
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
482a6a101c 1.1.0
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-05-30 22:49:39 +02:00
929f250006 feat(gaurds): use better smartguards to verify action authorization 2024-05-30 22:49:39 +02:00
30 changed files with 1506 additions and 587 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@serve.zone/cloudly",
"version": "1.0.216",
"version": "1.1.2",
"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.",
"type": "module",
@ -26,14 +26,15 @@
"@git.zone/tstest": "^1.0.90",
"@git.zone/tswatch": "^2.0.23",
"@push.rocks/tapbundle": "^5.0.23",
"@types/node": "^20.12.12"
"@types/node": "^20.13.0"
},
"dependencies": {
"@api.global/typedrequest": "3.0.25",
"@api.global/typedrequest": "3.0.30",
"@api.global/typedserver": "^3.0.50",
"@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.0.1",
"@apiclient.xyz/digitalocean": "^1.0.5",
"@apiclient.xyz/docker": "^1.0.112",
"@apiclient.xyz/hetznercloud": "^1.0.18",
"@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.0.289",
@ -47,11 +48,11 @@
"@push.rocks/smartacme": "^4.0.8",
"@push.rocks/smartbucket": "^3.0.9",
"@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/smartexit": "^1.0.23",
"@push.rocks/smartfile": "^11.0.15",
"@push.rocks/smartguard": "^2.0.1",
"@push.rocks/smartfile": "^11.0.16",
"@push.rocks/smartguard": "^3.0.2",
"@push.rocks/smartjson": "^5.0.19",
"@push.rocks/smartjwt": "^2.0.4",
"@push.rocks/smartlog": "^3.0.6",
@ -61,11 +62,12 @@
"@push.rocks/smartrequest": "^2.0.22",
"@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstream": "^3.0.39",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^3.0.2",
"@push.rocks/webjwt": "^1.0.9",
"@serve.zone/interfaces": "^1.0.56",
"@serve.zone/interfaces": "^1.0.62",
"@tsclass/tsclass": "^4.0.54"
},
"files": [

1454
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/cloudly',
version: '1.0.216',
version: '1.1.2',
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

@ -21,7 +21,8 @@ import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'
import { CloudlyServerManager } from './manager.server/servermanager.js';
import { ExternalApiManager } from './manager.status/statusmanager.js';
import { ImageManager } from './manager.image/classes.imagemanager.js';
import { logger } from './cloudly.logging.js';
import { logger } from './logger.js';
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
/**
* Cloudly class can be used to instantiate a cloudly server.
@ -48,6 +49,7 @@ export class Cloudly {
public mongodbConnector: MongodbConnector;
// managers
public authManager: CloudlyAuthManager;
public secretManager: CloudlySecretManager;
public clusterManager: ClusterManager;
public coreflowManager: CloudlyCoreflowManager;
@ -71,7 +73,8 @@ export class Cloudly {
this.cloudflareConnector = new CloudflareConnector(this);
this.letsencryptConnector = new LetsencryptConnector(this);
// processes
// managers
this.authManager = new CloudlyAuthManager(this);
this.clusterManager = new ClusterManager(this);
this.coreflowManager = new CloudlyCoreflowManager(this);
this.externalApiManager = new ExternalApiManager(this);
@ -90,6 +93,7 @@ export class Cloudly {
await this.config.init();
// manageers
await this.authManager.start();
await this.secretManager.start();
await this.serverManager.start();

View File

@ -1,6 +1,6 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { logger } from './cloudly.logging.js';
import { logger } from './logger.js';
import type { Cloudly } from './classes.cloudly.js';
/**
@ -11,9 +11,6 @@ export class CloudlyConfig {
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
public data: plugins.servezoneInterfaces.data.ICloudlyConfig
// authentication and settings
public smartjwtInstance: plugins.smartjwt.SmartJwt;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
@ -56,19 +53,8 @@ export class CloudlyConfig {
],
});
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
const kvStore = await this.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeys', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
this.data = await kvStore.readAll();
const missingKeys = await this.appData.logMissingKeys();
if (missingKeys.length > 0) {

View File

@ -1,7 +1,7 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { Cloudly } from './classes.cloudly.js';
import { logger } from './cloudly.logging.js';
import { logger } from './logger.js';
/**
* handles incoming requests from CI to deploy new versions of apps

View File

@ -34,7 +34,10 @@ export class LetsencryptConnector {
},
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

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

@ -0,0 +1,12 @@
import * as plugins from '../plugins.js';
export const users: plugins.servezoneInterfaces.data.IUser[] = [
{
id: 'user1',
data: {
username: 'admin',
password: 'password',
role: 'admin',
}
}
]

View File

@ -35,4 +35,31 @@ export const installDemoData = async (cloudlyRef: Cloudly) => {
await cluster.delete();
}
// ================================================================================
// USERS
const users = await cloudlyRef.authManager.CUser.getInstances({});
for (const user of users) {
await user.delete();
}
const demoDataUsers = await import('./demo.data.users.js');
for (const user of demoDataUsers.users) {
const userInstance = new cloudlyRef.authManager.CUser();
Object.assign(userInstance, user);
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

@ -3,7 +3,7 @@ early.start('cloudly');
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { Cloudly } from './classes.cloudly.js';
import { logger } from './cloudly.logging.js';
import { logger } from './logger.js';
const cloudlyQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir, true);
early.stop();

View File

@ -1,9 +1,17 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import type { Cloudly } from '../classes.cloudly.js';
import { logger } from '../logger.js';
import { Authorization } from './classes.authorization.js';
import { User } from './classes.user.js';
export class AuthManager {
export interface IJwtData {
userId: string;
status: 'loggedIn' | 'loggedOut';
}
export class CloudlyAuthManager {
cloudlyRef: Cloudly
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@ -11,7 +19,63 @@ export class AuthManager {
public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User);
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
}
public async start() {
// lets setup the smartjwtInstance
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
await this.smartjwtInstance.init();
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = await kvStore.readKey('jwtKeys');
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeys', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
if (!user) {
logger.log('warn', 'login failed');
} else {
jwt = await this.smartjwtInstance.createJWT({
userId: user.id,
status: 'loggedIn',
});
logger.log('success', 'login successful');
}
return {
jwt,
};
}
)
);
}
public async stop () {}
public adminJwtGuard = new plugins.smartguard.Guard<{jwt: string}>(async (dataArg) => {
const jwt = dataArg.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.'
})
}

View File

@ -1,6 +1,27 @@
import * as plugins from '../plugins.js';
@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) {
return await User.getInstance({
data: {
username: usernameArg,
password: passwordArg,
},
});
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: {
role: 'admin' | 'user';
username: string;
password: string;
};
}

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Cloudly } from '../classes.cloudly.js';
import { logger } from '../cloudly.logging.js';
import { logger } from '../logger.js';
import { Cluster } from './cluster.js';

View File

@ -26,8 +26,12 @@ export class CloudlyCoreflowManager {
return {
clusterIdentifier: {
clusterId: clusterConfig.id,
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');
return {
configData: await clusterConfigSet.createSavableObject()
configData: await clusterConfigSet.createSavableObject(),
deploymentDirectives: [],
};
}
)

View File

@ -1,12 +1,20 @@
import * as plugins from '../plugins.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> {
public static async create(imageDataArg: Partial<plugins.servezoneInterfaces.data.IImage['data']>) {
const image = new Image();
image.id = plugins.smartunique.uni('image');
Object.assign(image.data, imageDataArg);
image.id = await this.getNewId();
console.log(imageDataArg);
Object.assign(image, {
data: {
name: imageDataArg.name,
description: imageDataArg.description,
versions: [],
},
});
console.log((Image as any).saveableProperties)
await image.save();
return image;
}
@ -18,4 +26,12 @@ export class Image extends plugins.smartdata.SmartDataDbDoc<Image, plugins.serve
public data: plugins.servezoneInterfaces.data.IImage['data'];
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,23 +5,58 @@ import { Image } from './classes.image.js';
export class ImageManager {
cloudlyRef: Cloudly;
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartbucketInstance: plugins.smartbucket.SmartBucket;
public imageDir: plugins.smartbucket.Directory;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public typedrouter = new plugins.typedrequest.TypedRouter();
public CImage = plugins.smartdata.setDefaultManagerForDoc(this, Image);
smartbucketInstance: plugins.smartbucket.SmartBucket;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_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(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
'getAllImages',
async (requestArg) => {
async (requestArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], requestArg);
const images = await this.CImage.getInstances({});
return {
images: await Promise.all(
@ -35,22 +70,8 @@ export class ImageManager {
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
'createImage',
async (reqArg) => {
const image = await this.CImage.create({
name: reqArg.name,
});
return {
image: await image.createSavableObject(),
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PushImage>(
'pushImage',
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PushImageVersion>(
'pushImageVersion',
async (reqArg) => {
const pushStream = reqArg.imageStream;
return {};
@ -59,15 +80,14 @@ export class ImageManager {
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImage>(
'pullImage',
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_PullImageVersion>(
'pullImageVersion',
async (reqArg) => {
const image = await this.CImage.getInstance({
data: {
name: reqArg.name,
},
id: reqArg.imageId,
});
const imageVersion = null;
const imageVersion = image.data.versions.find((version) => version.versionString === reqArg.versionString);
const readable = this.imageDir.fastGetStream(await image.getStoragePath(reqArg.versionString));
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
return {
imageStream: imageVirtualStream,
@ -86,6 +106,10 @@ export class ImageManager {
);
const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test');
await bucket.fastPut({ path: 'test/test.txt', contents: 'hello' });
this.imageDir = await bucket.getDirectoryFromPath({
path: 'images',
});
}
public async createImage(nameArg: string) {

View File

@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { SecretBundle } from './classes.secretbundle.js';
import { SecretGroup } from './classes.secretgroup.js';
import { logger } from '../cloudly.logging.js';
import { logger } from '../logger.js';
import type { Cloudly } from '../classes.cloudly.js';
/**
@ -35,31 +35,11 @@ export class CloudlySecretManager {
this.typedrouter = new plugins.typedrequest.TypedRouter();
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
// console.log(dataArg);
if (dataArg.username !== 'admin' || dataArg.password !== 'password') {
logger.log('warn', 'login failed');
} else {
jwt = await this.cloudlyRef.config.smartjwtInstance.createJWT({
status: 'loggedIn',
});
logger.log('success', 'login successful');
}
return {
jwt,
};
}
)
);
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.secret.IReq_Admin_GetConfigBundlesAndSecretGroups>(
'adminGetConfigBundlesAndSecretGroups',
async (dataArg) => {
async (dataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminJwtGuard], dataArg);
dataArg.jwt
const secretBundles = await SecretBundle.getInstances({});
const secretGroups = await SecretGroup.getInstances({});

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { logger } from '../cloudly.logging.js';
import { logger } from '../logger.js';
export class CloudlyTaskmanager {
public cloudlyRef: Cloudly;

View File

@ -32,7 +32,6 @@ import * as smartcli from '@push.rocks/smartcli';
import * as smartdata from '@push.rocks/smartdata';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartexit from '@push.rocks/smartexit';
import * as typedserver from '@api.global/typedserver';
import * as smartfile from '@push.rocks/smartfile';
import * as smartguard from '@push.rocks/smartguard';
import * as smartjson from '@push.rocks/smartjson';
@ -45,6 +44,7 @@ import * as smartssh from '@push.rocks/smartssh';
import * as smartstring from '@push.rocks/smartstring';
import * as smartunique from '@push.rocks/smartunique';
import * as taskbuffer from '@push.rocks/taskbuffer';
import * as typedserver from '@api.global/typedserver';
export {
npmextra,
@ -55,7 +55,6 @@ export {
smartcli,
smartdata,
smartexit,
typedserver,
smartdelay,
smartfile,
smartguard,
@ -69,6 +68,7 @@ export {
smartstring,
smartunique,
taskbuffer,
typedserver,
};
// @servezone scope

View File

@ -2,6 +2,8 @@ import * as plugins from './plugins.js';
export type TClientType = 'coreflow' | 'cli' | 'serverconfig';
import { Image } from './classes.image.js';
export class CloudlyClient {
private cloudlyUrl: string;
private registerAs: string;
@ -55,15 +57,18 @@ export class CloudlyClient {
this.typedrouter,
this.cloudlyUrl
);
console.log(`CloudlyCluent connected to cloudly at ${this.cloudlyUrl}. Remember to get an identity.`)
}
public async stop() {
await this.typedsocketClient.stop();
}
public identity: plugins.servezoneInterfaces.data.IClusterIdentifier;
public async getIdentityByJumpCode(
jumpCodeArg: string,
tagConnection = false
tagConnection = false,
statefullIdentity = true
): Promise<plugins.servezoneInterfaces.data.IClusterIdentifier> {
const identityRequest =
this.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByJumpCode>(
@ -80,6 +85,10 @@ export class CloudlyClient {
this.typedsocketClient.addTag('identity', identity);
}
if (statefullIdentity) {
this.identity = identity;
}
return identity;
}
@ -91,7 +100,7 @@ export class CloudlyClient {
'getClusterConfig'
);
const response = await clusterConfigRequest.fire({
jwt: '', // TODO: do proper auth here
jwt: '',
clusterIdentifier: identityArg,
});
return response.configData;
@ -127,4 +136,9 @@ export class CloudlyClient {
});
return typedResponse.certificate;
}
// Images
public async getImages() {
return Image.getImages(this);
}
}

View File

@ -1,5 +1,84 @@
import type { CloudlyClient } from './classes.cloudlyclient.js';
import * as plugins from './plugins.js';
export class Image {
public getImages() {}
export class Image implements plugins.servezoneInterfaces.data.IImage {
public static async getImages(cloudlyClientRef: CloudlyClient) {
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: CloudlyClient;
id: plugins.servezoneInterfaces.data.IImage['id'];
data: plugins.servezoneInterfaces.data.IImage['data'];
constructor(cloudlyClientRef: CloudlyClient) {
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

@ -6,10 +6,14 @@ export {
}
// @push.rocks scope
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrx from '@push.rocks/smartrx';
import * as webstream from '@push.rocks/smartstream/web';
export {
smartpromise,
smartrx,
webstream,
}
// @api.global scope

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/cloudly',
version: '1.0.216',
version: '1.1.2',
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

@ -72,7 +72,7 @@ export const dataState = await appstate.getStatePart<IDataState>(
);
// Getting data
export const getDataAction = dataState.createAction(async (statePartArg) => {
export const getAllDataAction = dataState.createAction(async (statePartArg, partialArg?: 'secrets' | 'images') => {
let currentState = statePartArg.getState();
// Secrets
const trGetSecrets =
@ -88,6 +88,20 @@ export const getDataAction = dataState.createAction(async (statePartArg) => {
...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
const trGetClusters =
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_GetAllClusters>(
@ -120,7 +134,7 @@ export const createSecretGroupAction = dataState.createAction(
secretBundles: [],
secretGroups: [payloadArg],
});
currentState = await dataState.dispatchAction(getDataAction, null);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
return currentState;
}
@ -139,7 +153,7 @@ export const deleteSecretGroupAction = dataState.createAction(
secretBundleIds: [],
secretGroupIds: [payloadArg.secretGroupId],
});
currentState = await dataState.dispatchAction(getDataAction, null);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
@ -158,7 +172,53 @@ export const deleteSecretBundleAction = dataState.createAction(
secretBundleIds: [payloadArg.configBundleId],
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;
}
);

View File

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

View File

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

View File

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