c42557a86b
- New HelmService wrapping Helm CLI (install/upgrade, rollback, uninstall, history, status) - deleteApplication: helm uninstall + explicit cleanup of kept PVCs, secrets, TLS certs - buildHelmValues: include registry.url for pull secret - Add scaleDeployment for lifecycle suspend/resume - Unit tests for buildHelmValues and HelmService
191 lines
6.6 KiB
TypeScript
191 lines
6.6 KiB
TypeScript
import { AppRuntime, DatabaseType } from '../common/enums';
|
|
|
|
/**
|
|
* Tests for KubernetesService.buildHelmValues (private method).
|
|
* We extract and test the logic directly since it's critical for Helm deployments.
|
|
*/
|
|
describe('buildHelmValues logic', () => {
|
|
const domain = 'apps.cloudhost.local';
|
|
|
|
function buildHelmValues(app: any, imageUri: string): Record<string, any> {
|
|
const isWordPress = app.runtime === AppRuntime.WORDPRESS;
|
|
const hasDb = app.databaseType !== DatabaseType.NONE;
|
|
const isPostgres = app.databaseType === DatabaseType.POSTGRESQL;
|
|
|
|
return {
|
|
app: {
|
|
name: app.name,
|
|
namespace: `user-${app.userId.split('-')[0]}`,
|
|
runtime: app.runtime,
|
|
image: imageUri,
|
|
port: app.port,
|
|
replicas: app.replicas,
|
|
},
|
|
resources: {
|
|
cpuRequest: app.cpuRequest,
|
|
cpuLimit: app.cpuLimit,
|
|
memoryRequest: app.memoryRequest,
|
|
memoryLimit: app.memoryLimit,
|
|
},
|
|
envVars: app.envVars || {},
|
|
ingress: {
|
|
enabled: true,
|
|
subdomain: app.subdomain || app.name,
|
|
domain: domain,
|
|
clusterIssuer: 'letsencrypt-prod',
|
|
},
|
|
database: {
|
|
enabled: hasDb,
|
|
type: app.databaseType,
|
|
version: app.dbVersion || (isPostgres ? '16' : '8.0'),
|
|
username: app.dbUsername || 'appuser',
|
|
password: app.dbPassword || 'generated-password',
|
|
storageSize: app.dbStorageSize || '1Gi',
|
|
resources: {
|
|
cpuRequest: '100m',
|
|
cpuLimit: '500m',
|
|
memoryRequest: '256Mi',
|
|
memoryLimit: '512Mi',
|
|
},
|
|
},
|
|
wordpress: {
|
|
enabled: isWordPress,
|
|
wpContentStorageSize: '2Gi',
|
|
},
|
|
changeCause: `Deploy ${imageUri} at 2024-01-01T00:00:00.000Z`,
|
|
};
|
|
}
|
|
|
|
const baseApp = {
|
|
name: 'my-app',
|
|
userId: 'abc123-def456',
|
|
runtime: AppRuntime.NODEJS,
|
|
port: 3000,
|
|
replicas: 1,
|
|
cpuRequest: '100m',
|
|
cpuLimit: '500m',
|
|
memoryRequest: '128Mi',
|
|
memoryLimit: '512Mi',
|
|
databaseType: DatabaseType.NONE,
|
|
envVars: {},
|
|
subdomain: 'my-app-abc123',
|
|
};
|
|
|
|
it('should set correct namespace from userId', () => {
|
|
const values = buildHelmValues(baseApp, 'registry/my-app:123');
|
|
expect(values.app.namespace).toBe('user-abc123');
|
|
});
|
|
|
|
it('should disable database when type is NONE', () => {
|
|
const values = buildHelmValues(baseApp, 'registry/my-app:123');
|
|
expect(values.database.enabled).toBe(false);
|
|
});
|
|
|
|
it('should enable database for PostgreSQL', () => {
|
|
const app = { ...baseApp, databaseType: DatabaseType.POSTGRESQL, dbUsername: 'pguser', dbPassword: 'secret' };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.database.enabled).toBe(true);
|
|
expect(values.database.type).toBe('postgresql');
|
|
expect(values.database.version).toBe('16');
|
|
expect(values.database.username).toBe('pguser');
|
|
});
|
|
|
|
it('should enable database for MySQL with correct default version', () => {
|
|
const app = { ...baseApp, databaseType: DatabaseType.MYSQL };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.database.enabled).toBe(true);
|
|
expect(values.database.version).toBe('8.0');
|
|
});
|
|
|
|
it('should enable wordpress flags for wordpress runtime', () => {
|
|
const app = { ...baseApp, runtime: AppRuntime.WORDPRESS, databaseType: DatabaseType.MYSQL };
|
|
const values = buildHelmValues(app, 'registry/wp:1');
|
|
expect(values.wordpress.enabled).toBe(true);
|
|
expect(values.database.enabled).toBe(true);
|
|
});
|
|
|
|
it('should not enable wordpress for nodejs runtime', () => {
|
|
const values = buildHelmValues(baseApp, 'registry/my-app:123');
|
|
expect(values.wordpress.enabled).toBe(false);
|
|
});
|
|
|
|
it('should use subdomain from app if provided', () => {
|
|
const values = buildHelmValues(baseApp, 'registry/my-app:123');
|
|
expect(values.ingress.subdomain).toBe('my-app-abc123');
|
|
});
|
|
|
|
it('should fallback subdomain to app name', () => {
|
|
const app = { ...baseApp, subdomain: undefined };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.ingress.subdomain).toBe('my-app');
|
|
});
|
|
|
|
it('should use custom dbVersion when provided', () => {
|
|
const app = { ...baseApp, databaseType: DatabaseType.POSTGRESQL, dbVersion: '15' };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.database.version).toBe('15');
|
|
});
|
|
|
|
it('should default dbStorageSize to 1Gi', () => {
|
|
const app = { ...baseApp, databaseType: DatabaseType.POSTGRESQL };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.database.storageSize).toBe('1Gi');
|
|
});
|
|
|
|
it('should use custom dbStorageSize when provided', () => {
|
|
const app = { ...baseApp, databaseType: DatabaseType.POSTGRESQL, dbStorageSize: '5Gi' };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.database.storageSize).toBe('5Gi');
|
|
});
|
|
|
|
it('should pass envVars as empty object when not set', () => {
|
|
const app = { ...baseApp, envVars: undefined };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.envVars).toEqual({});
|
|
});
|
|
|
|
it('should pass envVars when set', () => {
|
|
const app = { ...baseApp, envVars: { NODE_ENV: 'production', API_KEY: '12345' } };
|
|
const values = buildHelmValues(app, 'registry/my-app:123');
|
|
expect(values.envVars).toEqual({ NODE_ENV: 'production', API_KEY: '12345' });
|
|
});
|
|
|
|
it('should set image correctly in app values', () => {
|
|
const values = buildHelmValues(baseApp, 'registry.local:5000/abc123/my-app:1700000000');
|
|
expect(values.app.image).toBe('registry.local:5000/abc123/my-app:1700000000');
|
|
});
|
|
});
|
|
|
|
describe('generatePassword', () => {
|
|
function generatePassword(length = 24): string {
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
let password = '';
|
|
for (let i = 0; i < length; i++) {
|
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return password;
|
|
}
|
|
|
|
it('should generate password of specified length', () => {
|
|
expect(generatePassword(16)).toHaveLength(16);
|
|
expect(generatePassword(32)).toHaveLength(32);
|
|
expect(generatePassword()).toHaveLength(24);
|
|
});
|
|
|
|
it('should only contain alphanumeric characters (no shell-unsafe chars)', () => {
|
|
for (let i = 0; i < 100; i++) {
|
|
const pw = generatePassword();
|
|
expect(pw).toMatch(/^[a-zA-Z0-9]+$/);
|
|
}
|
|
});
|
|
|
|
it('should generate unique passwords', () => {
|
|
const passwords = new Set<string>();
|
|
for (let i = 0; i < 50; i++) {
|
|
passwords.add(generatePassword());
|
|
}
|
|
// With 62^24 possibilities, all 50 should be unique
|
|
expect(passwords.size).toBe(50);
|
|
});
|
|
});
|