f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as helpers from './helpers/index.js';
|
|
|
|
import * as cloudly from '../ts/index.js';
|
|
import * as cloudlyApiClient from '@serve.zone/api';
|
|
|
|
let testCloudly: cloudly.Cloudly;
|
|
let testClient: cloudlyApiClient.CloudlyApiClient;
|
|
|
|
const logErrorDetails = (errorArg: unknown) => {
|
|
if (errorArg instanceof Error) {
|
|
console.error(` - Error message: ${errorArg.message}`);
|
|
console.error(` - Error stack:`, errorArg.stack);
|
|
return;
|
|
}
|
|
console.error(` - Error:`, errorArg);
|
|
};
|
|
|
|
tap.preTask('should start cloudly', async () => {
|
|
testCloudly = await helpers.createCloudly();
|
|
await testCloudly.start();
|
|
});
|
|
|
|
tap.preTask('should create a new machine user for testing', async () => {
|
|
console.log('🔵 PreTask: Creating first machine user...');
|
|
const machineUser = new testCloudly.authManager.CUser();
|
|
machineUser.id = await testCloudly.authManager.CUser.getNewId();
|
|
console.log(` - User ID: ${machineUser.id}`);
|
|
machineUser.data = {
|
|
type: 'machine',
|
|
username: 'test',
|
|
password: 'test',
|
|
tokens: [{
|
|
token: 'test',
|
|
expiresAt: Date.now() + 3600 * 1000 * 24 * 365,
|
|
assignedRoles: ['admin'],
|
|
}],
|
|
role: 'admin',
|
|
};
|
|
console.log(` - Username: ${machineUser.data.username}`);
|
|
console.log(` - Role: ${machineUser.data.role}`);
|
|
console.log(` - Token: 'test'`);
|
|
console.log(` - Token roles: ${machineUser.data.tokens?.[0]?.assignedRoles?.join(', ') ?? ''}`);
|
|
await machineUser.save();
|
|
console.log('✅ PreTask: First machine user saved successfully');
|
|
});
|
|
|
|
tap.test('should create a new cloudlyApiClient', async () => {
|
|
console.log('🔵 Test: Creating CloudlyApiClient...');
|
|
testClient = new cloudlyApiClient.CloudlyApiClient({
|
|
registerAs: 'api',
|
|
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
|
});
|
|
console.log(` - URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
|
await testClient.start();
|
|
console.log('✅ CloudlyApiClient started successfully');
|
|
expect(testClient).toBeTruthy();
|
|
});
|
|
|
|
tap.test('DEBUG: Check existing users', async () => {
|
|
console.log('🔍 DEBUG: Checking existing users in database...');
|
|
const allUsers = await testCloudly.authManager.CUser.getInstances({});
|
|
console.log(` - Total users found: ${allUsers.length}`);
|
|
for (const user of allUsers) {
|
|
console.log(` - User: ${user.data.username} (ID: ${user.id})`);
|
|
console.log(` - Type: ${user.data.type}`);
|
|
console.log(` - Role: ${user.data.role}`);
|
|
console.log(` - Tokens: ${user.data.tokens?.length ?? 0}`);
|
|
for (const token of user.data.tokens ?? []) {
|
|
console.log(` - Token: '${token.token}' | Roles: ${token.assignedRoles?.join(', ')}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.test('should get an identity', async () => {
|
|
console.log('🔵 Test: Getting identity by token...');
|
|
console.log(` - Using token: 'test'`);
|
|
console.log(` - API URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
|
|
|
try {
|
|
const identity = await testClient.getIdentityByToken('test');
|
|
console.log('✅ Identity retrieved successfully:');
|
|
console.log(` - Identity exists: ${!!identity}`);
|
|
if (identity) {
|
|
console.log(` - Identity data:`, JSON.stringify(identity, null, 2));
|
|
}
|
|
expect(identity).toBeTruthy();
|
|
} catch (error) {
|
|
console.error('❌ Failed to get identity:');
|
|
logErrorDetails(error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('should expose the OCI registry endpoint', async () => {
|
|
const response = await fetch(
|
|
`http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}/v2/`,
|
|
);
|
|
expect(response.status).toEqual(200);
|
|
expect(response.headers.get('docker-distribution-api-version')).toEqual('registry/2.0');
|
|
});
|
|
|
|
tap.test('should require authentication for OCI registry tokens', async () => {
|
|
const response = await fetch(
|
|
`http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}/v2/token?service=cloudly&scope=repository:test/app:pull`,
|
|
);
|
|
expect(response.status).toEqual(401);
|
|
});
|
|
|
|
tap.test('should issue OCI registry tokens for the initial admin', async () => {
|
|
const credentials = Buffer.from(
|
|
`${helpers.testCloudlyAdminAccount.username}:${helpers.testCloudlyAdminAccount.password}`,
|
|
).toString('base64');
|
|
const response = await fetch(
|
|
`http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}/v2/token?service=cloudly&scope=repository:test/app:pull,push`,
|
|
{
|
|
headers: {
|
|
Authorization: `Basic ${credentials}`,
|
|
},
|
|
},
|
|
);
|
|
const body = await response.json();
|
|
expect(response.status).toEqual(200);
|
|
expect(body.token).toBeTruthy();
|
|
expect(body.access_token).toEqual(body.token);
|
|
});
|
|
|
|
tap.test('should deny OCI registry push tokens for non-admin users', async () => {
|
|
const readonlyUsername = 'registry-readonly';
|
|
const readonlyToken = 'registry-readonly-token';
|
|
const readonlyUser = new testCloudly.authManager.CUser();
|
|
readonlyUser.id = await testCloudly.authManager.CUser.getNewId();
|
|
readonlyUser.data = {
|
|
type: 'machine',
|
|
username: readonlyUsername,
|
|
password: readonlyToken,
|
|
tokens: [{
|
|
token: readonlyToken,
|
|
expiresAt: Date.now() + 3600 * 1000,
|
|
assignedRoles: [],
|
|
}],
|
|
role: 'user',
|
|
};
|
|
await readonlyUser.save();
|
|
|
|
const credentials = Buffer.from(`${readonlyUsername}:${readonlyToken}`).toString('base64');
|
|
const pullResponse = await fetch(
|
|
`http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}/v2/token?service=cloudly&scope=repository:test/readonly:pull`,
|
|
{
|
|
headers: {
|
|
Authorization: `Basic ${credentials}`,
|
|
},
|
|
},
|
|
);
|
|
expect(pullResponse.status).toEqual(200);
|
|
|
|
const pushResponse = await fetch(
|
|
`http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}/v2/token?service=cloudly&scope=repository:test/readonly:pull,push`,
|
|
{
|
|
headers: {
|
|
Authorization: `Basic ${credentials}`,
|
|
},
|
|
},
|
|
);
|
|
expect(pushResponse.status).toEqual(403);
|
|
});
|
|
|
|
tap.test('should expose generated service registry targets', async () => {
|
|
const image = await testClient.image.createImage({
|
|
name: 'Registry Target Test Image',
|
|
description: 'Image used by the registry target test',
|
|
});
|
|
const service = await testClient.services.createService({
|
|
name: 'Registry Target Test Service',
|
|
description: 'Service used by the registry target test',
|
|
imageId: image.id,
|
|
imageVersion: 'latest',
|
|
environment: {},
|
|
secretBundleId: '',
|
|
serviceCategory: 'workload',
|
|
deploymentStrategy: 'custom',
|
|
scaleFactor: 1,
|
|
balancingStrategy: 'round-robin',
|
|
ports: {
|
|
web: 3000,
|
|
},
|
|
domains: [],
|
|
deploymentIds: [],
|
|
});
|
|
|
|
const registryTarget = await testClient.services.getRegistryTarget(service.id, 'latest');
|
|
expect(registryTarget.protocol).toEqual('oci');
|
|
expect(registryTarget.registryHost).toEqual(`${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
|
expect(registryTarget.repository.startsWith('workloads/registry-target-test-service-')).toBeTrue();
|
|
expect(registryTarget.repository.split('/').every((partArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(partArg))).toBeTrue();
|
|
expect(registryTarget.imageUrl).toEqual(`${registryTarget.registryHost}/${registryTarget.repository}:latest`);
|
|
|
|
const refreshedService = await testClient.services.getServiceById(service.id);
|
|
expect(refreshedService.data.registryTarget?.imageUrl).toEqual(registryTarget.imageUrl);
|
|
});
|
|
|
|
tap.test('should trim truncated registry repository suffixes', async () => {
|
|
const registryTarget = testCloudly.registryManager.getServiceRegistryTarget({
|
|
id: 'service-5gv-123456',
|
|
data: {
|
|
name: 'Registry Target Test Service',
|
|
},
|
|
} as any);
|
|
|
|
expect(registryTarget.repository).toEqual('workloads/registry-target-test-service-service-5gv');
|
|
expect(registryTarget.repository.split('/').every((partArg) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(partArg))).toBeTrue();
|
|
});
|
|
|
|
tap.test('should push service config updates to connected coreflows', async (toolsArg) => {
|
|
const cluster = await testClient.cluster.createCluster('Registry Config Push Test Cluster');
|
|
const persistedCluster = await testCloudly.clusterManager.getConfigBy_ConfigID(cluster.id);
|
|
const clusterUser = await testCloudly.authManager.CUser.getInstance({
|
|
id: persistedCluster.data.userId,
|
|
});
|
|
const clusterToken = clusterUser.data.tokens?.[0]?.token;
|
|
expect(clusterToken).toBeTruthy();
|
|
const coreflowClient = new cloudlyApiClient.CloudlyApiClient({
|
|
registerAs: 'coreflow',
|
|
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
|
});
|
|
const configUpdates: any[] = [];
|
|
let subscription: { unsubscribe: () => void } | undefined;
|
|
|
|
try {
|
|
await coreflowClient.start();
|
|
await coreflowClient.getIdentityByToken(clusterToken!, {
|
|
statefullIdentity: true,
|
|
tagConnection: true,
|
|
});
|
|
subscription = coreflowClient.configUpdateSubject.subscribe((updateArg) => {
|
|
configUpdates.push(updateArg);
|
|
});
|
|
|
|
const image = await testClient.image.createImage({
|
|
name: 'Registry Config Push Test Image',
|
|
description: 'Image used by the config push test',
|
|
});
|
|
const service = await testClient.services.createService({
|
|
name: 'Registry Config Push Test Service',
|
|
description: 'Service used by the config push test',
|
|
imageId: image.id,
|
|
imageVersion: 'latest',
|
|
environment: {},
|
|
secretBundleId: '',
|
|
serviceCategory: 'workload',
|
|
deploymentStrategy: 'custom',
|
|
scaleFactor: 1,
|
|
balancingStrategy: 'round-robin',
|
|
ports: {
|
|
web: 3000,
|
|
},
|
|
domains: [],
|
|
deploymentIds: [],
|
|
});
|
|
|
|
await toolsArg.delayFor(100);
|
|
expect(configUpdates[0]?.configData.id).toEqual(cluster.id);
|
|
expect(configUpdates[0]?.services.find((serviceArg: any) => serviceArg.id === service.id)).toBeTruthy();
|
|
} finally {
|
|
subscription?.unsubscribe();
|
|
await coreflowClient.stop();
|
|
}
|
|
});
|
|
|
|
tap.test('should allow cluster coreflows to read deployment inputs', async () => {
|
|
const cluster = await testClient.cluster.createCluster('Registry Coreflow Read Test Cluster');
|
|
const persistedCluster = await testCloudly.clusterManager.getConfigBy_ConfigID(cluster.id);
|
|
const clusterUser = await testCloudly.authManager.CUser.getInstance({
|
|
id: persistedCluster.data.userId,
|
|
});
|
|
const clusterToken = clusterUser.data.tokens?.[0]?.token;
|
|
expect(clusterToken).toBeTruthy();
|
|
|
|
const image = await testClient.image.createImage({
|
|
name: 'Registry Coreflow Read Test Image',
|
|
description: 'Image used by the coreflow read test',
|
|
});
|
|
const secretBundle = await testClient.secretbundle.createSecretBundle({
|
|
name: 'Registry Coreflow Read Test Secret Bundle',
|
|
description: 'Secret bundle used by the coreflow read test',
|
|
type: 'service',
|
|
includedSecretGroupIds: [],
|
|
includedTags: [],
|
|
imageClaims: [],
|
|
authorizations: [
|
|
{
|
|
environment: 'production',
|
|
secretAccessKey: 'registry-coreflow-read-test',
|
|
},
|
|
],
|
|
});
|
|
|
|
const coreflowClient = new cloudlyApiClient.CloudlyApiClient({
|
|
registerAs: 'coreflow',
|
|
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
|
});
|
|
|
|
try {
|
|
await coreflowClient.start();
|
|
await coreflowClient.getIdentityByToken(clusterToken!, {
|
|
statefullIdentity: true,
|
|
tagConnection: true,
|
|
});
|
|
const clusterImage = await coreflowClient.image.getImageById(image.id);
|
|
const clusterSecretBundle = await coreflowClient.secretbundle.getSecretBundleById(secretBundle.id);
|
|
expect(clusterImage.id).toEqual(image.id);
|
|
expect(clusterSecretBundle.id).toEqual(secretBundle.id);
|
|
} finally {
|
|
await coreflowClient.stop();
|
|
}
|
|
});
|
|
|
|
tap.test('should expose platform desired state', async () => {
|
|
const capabilitiesResponse = await testClient.platform.getPlatformCapabilities();
|
|
expect(capabilitiesResponse.capabilities.find((capability) => capability.id === 'database')).toBeTruthy();
|
|
|
|
const desiredState = await testClient.platform.getPlatformDesiredState();
|
|
expect(desiredState.capabilities).toBeTruthy();
|
|
expect(desiredState.providerConfigs).toBeTruthy();
|
|
expect(desiredState.bindings).toBeTruthy();
|
|
});
|
|
|
|
let platformProviderConfigId: string;
|
|
let platformBindingId: string;
|
|
tap.test('should upsert platform provider config and binding', async () => {
|
|
const providerConfigResponse = await testClient.platform.upsertPlatformProviderConfig({
|
|
id: '',
|
|
capability: 'database',
|
|
providerType: 'docker',
|
|
name: 'Local Docker Database',
|
|
enabled: true,
|
|
});
|
|
platformProviderConfigId = providerConfigResponse.providerConfig.id;
|
|
expect(platformProviderConfigId).toBeTruthy();
|
|
|
|
const bindingResponse = await testClient.platform.upsertPlatformBinding({
|
|
id: '',
|
|
serviceId: 'test-service',
|
|
capability: 'database',
|
|
desiredState: 'enabled',
|
|
status: 'requested',
|
|
providerConfigId: platformProviderConfigId,
|
|
});
|
|
platformBindingId = bindingResponse.binding.id;
|
|
expect(platformBindingId).toBeTruthy();
|
|
|
|
const statusResponse = await testClient.platform.updatePlatformBindingStatus({
|
|
bindingId: platformBindingId,
|
|
status: 'ready',
|
|
endpoints: [
|
|
{
|
|
name: 'primary',
|
|
capability: 'database',
|
|
protocol: 'mongodb',
|
|
internalUrl: 'mongodb://platform-database:27017/test-service',
|
|
},
|
|
],
|
|
});
|
|
expect(statusResponse.binding.status).toEqual('ready');
|
|
|
|
const bindingsResponse = await testClient.platform.getPlatformBindings({
|
|
serviceId: 'test-service',
|
|
});
|
|
expect(bindingsResponse.bindings.find((binding) => binding.id === platformBindingId)).toBeTruthy();
|
|
});
|
|
|
|
let image: any;
|
|
tap.test('should create and upload an image', async () => {
|
|
console.log('🔵 Test: Creating and uploading image...');
|
|
console.log(` - Image name: 'test'`);
|
|
console.log(` - Image description: 'test'`);
|
|
|
|
try {
|
|
image = await testClient.image.createImage({
|
|
name: 'test',
|
|
description: 'test'
|
|
});
|
|
console.log('✅ Image created successfully:');
|
|
console.log(` - Image ID: ${image?.id}`);
|
|
console.log(` - Image data:`, image);
|
|
expect(image).toBeTruthy();
|
|
} catch (error) {
|
|
console.error('❌ Failed to create image:');
|
|
logErrorDetails(error);
|
|
throw error;
|
|
}
|
|
})
|
|
|
|
tap.test('should upload an image version', async () => {
|
|
console.log('🔵 Test: Uploading image version...');
|
|
console.log(` - Version: 'v1.0.0'`);
|
|
console.log(` - Image exists: ${!!image}`);
|
|
console.log(` - Image ID: ${image?.id}`);
|
|
|
|
try {
|
|
const imageStream = await helpers.getAlpineImageReadableStream();
|
|
console.log(' - Image stream obtained successfully');
|
|
|
|
await image.pushImageVersion('v1.0.0', imageStream);
|
|
console.log('✅ Image version uploaded successfully');
|
|
} catch (error) {
|
|
console.error('❌ Failed to upload image version:');
|
|
logErrorDetails(error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('should stop the apiclient', async (toolsArg) => {
|
|
await toolsArg.delayFor(10000);
|
|
await helpers.stopCloudly();
|
|
await testClient.stop();
|
|
await testCloudly.stop();
|
|
})
|
|
|
|
export default tap.start();
|