feat(k8s): add HelmService and rewrite deleteApplication
- 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
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user