Skip to content

Commit

Permalink
feat: preview deployment (#9)
Browse files Browse the repository at this point in the history
* create preview elements

* implement apply

* add testing
  • Loading branch information
buehler authored and mfeltscher committed Apr 25, 2019
1 parent a1bc4f7 commit 66d92ad
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 126 deletions.
85 changes: 7 additions & 78 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@
"@semantic-release/gitlab": "^3.1.2",
"@semantic-release/npm": "^5.1.4",
"@smartive/tslint-config": "^6.0.0",
"@types/clipboardy": "^1.1.0",
"@types/fs-extra": "^5.0.5",
"@types/got": "^9.4.1",
"@types/got": "^9.4.2",
"@types/inquirer": "6.0.0",
"@types/jest": "^24.0.11",
"@types/node": "^11.13.4",
Expand All @@ -75,7 +74,7 @@
"pkg": "^4.3.7",
"semantic-release": "^15.13.3",
"ts-jest": "^24.0.2",
"tslint": "^5.15.0",
"tslint": "^5.16.0",
"tsutils": "^3.10.0",
"typescript": "^3.4.3"
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/apply/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const applyCommand: CommandModule<RootArguments, ApplyArguments> = {
await kubeConfigCommand.handler({
...args,
noInteraction: true,
force: true,
});
}

Expand Down
1 change: 1 addition & 0 deletions src/commands/delete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const deleteCommand: CommandModule<RootArguments, DeleteArguments> = {
await kubeConfigCommand.handler({
...args,
noInteraction: true,
force: true,
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { kubeConfigCommand } from './kube-config';
import { kubectlCommand } from './kubectl';
import { namespaceCommand } from './namespace';
import { prepareCommand } from './prepare';
import { previewDeployCommand } from './preview-deploy';
import { secretCommand } from './secret';
import { versionCommand } from './version';

Expand All @@ -22,6 +23,7 @@ export const commands: CommandModule<any, any>[] = [
kubectlCommand,
namespaceCommand,
prepareCommand,
previewDeployCommand,
secretCommand,
versionCommand,
];
34 changes: 20 additions & 14 deletions src/commands/kube-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import chalk from 'chalk';
import { outputFile, pathExists } from 'fs-extra';
import { Arguments, Argv, CommandModule } from 'yargs';

Expand All @@ -13,6 +12,7 @@ const defaultEnv = 'KUBE_CONFIG';
type KubeConfigArguments = RootArguments & {
configContent?: string;
noInteraction: boolean;
force: boolean;
};

interface KubeConfigCommandModule extends CommandModule<RootArguments, KubeConfigArguments> {
Expand All @@ -23,16 +23,22 @@ export const kubeConfigCommand: KubeConfigCommandModule = {
command: 'kube-config [configContent]',
aliases: 'kc',
describe:
`Use the given kube-config content ${chalk.yellow('(base64 encoded)')} ` +
`and create the ~/.kube/config file. If the content is omitted, the content ` +
`of the env var "$KUBE_CONFIG" is used.`,
'Use the given kube-config content ' +
'and create the ~/.kube/config file. If the content is omitted, the content ' +
'of the env var "$KUBE_CONFIG" is used.',

builder: (argv: Argv<RootArguments>) =>
(argv
.positional('configContent', {
description: 'Base64 encoded kube-config content.',
description: 'kube-config content (base64 encoded or not).',
type: 'string',
})
.option('f', {
alias: 'force',
boolean: true,
default: false,
description: 'Force login, overwrite existing ~/.kube/config.',
})
.option('n', {
alias: 'no-interaction',
boolean: true,
Expand All @@ -50,36 +56,36 @@ export const kubeConfigCommand: KubeConfigCommandModule = {
return;
}

const content = (args.configContent || process.env[defaultEnv] || '').trim();
let content = (args.configContent || process.env[defaultEnv] || '').trim();

if (!content) {
logger.error('Config content is empty. Aborting.');
process.exit(ExitCode.error);
return;
}

if (!content.isBase64()) {
logger.error('The content is not base64 encoded. Aborting.');
process.exit(ExitCode.error);
return;
if (content.isBase64()) {
logger.info('The content was base64 encoded.');
content = content.base64Decode();
}

if (await pathExists(Filepathes.configPath)) {
if (args.noInteraction) {
if (args.noInteraction && !args.force) {
logger.info('Config already exists, exitting.');
return;
}

if (
!(await simpleConfirm('The kube config (~/.kube/config) already exists. ' + 'Do you want to overwrite it?', false))
!args.force &&
!(await simpleConfirm('The kube config (~/.kube/config) already exists. Do you want to overwrite it?', false))
) {
return;
}
}

logger.info('Writing ~/.kube/config file.');
await outputFile(Filepathes.configPath, content.base64Decode());
await outputFile(Filepathes.configPath, content);

logger.success('Login done.');
logger.success('~/.kube/config written.');
},
};
124 changes: 124 additions & 0 deletions src/commands/preview-deploy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { V1Namespace, V1ObjectMeta } from '@kubernetes/client-node';
import { async } from 'fast-glob';
import { readFile } from 'fs-extra';
import { EOL } from 'os';
import { posix } from 'path';
import { Arguments, Argv, CommandModule } from 'yargs';

import { RootArguments } from '../../root-arguments';
import { envsubst } from '../../utils/envsubst';
import { exec } from '../../utils/exec';
import { ExitCode } from '../../utils/exit-code';
import { KubernetesApi } from '../../utils/kubernetes-api';
import { Logger } from '../../utils/logger';
import { kubeConfigCommand } from '../kube-config';

type PreviewDeployArguments = RootArguments & {
name: string;
sourceFolder: string;
prefix: string;
kubeConfig?: string;
};

const logger = new Logger('preview deployment');

async function createNamespace(api: KubernetesApi, name: string): Promise<void> {
try {
logger.info(`Create preview namespace "${name}".`);
await api.core.readNamespace(name);
logger.info(`Namespace "${name}" already exists.`);
} catch (e) {
if (e.response.statusCode !== 404) {
throw e;
}

await api.core.createNamespace({
...new V1Namespace(),
apiVersion: 'v1',
kind: 'Namespace',
metadata: {
...new V1ObjectMeta(),
name,
},
});
logger.success(`Created namespace "${name}".`);
}
}

export const previewDeployCommand: CommandModule<RootArguments, PreviewDeployArguments> = {
command: 'preview-deploy <name> [sourceFolder]',
aliases: 'prev-dep',
describe:
'Create opinionated preview deployment. This command creates a namespace ' +
'and deploys the application in the given namespace.',

builder: (argv: Argv<RootArguments>) =>
argv
.option('prefix', {
string: true,
description: 'Defines the prefix for the namespace name.',
default: 'prev-',
})
.option('kube-config', {
string: true,
description: 'Define the kube config to use for namespace creation. Defaults to $KUBE_CONFIG variable.',
})
.positional('name', {
description: 'Name of the preview namespace.',
type: 'string',
})
.positional('sourceFolder', {
description: 'Folder to search for yaml files.',
type: 'string',
default: './k8s/',
}) as Argv<PreviewDeployArguments>,

async handler(args: Arguments<PreviewDeployArguments>): Promise<void> {
if (args.getYargsCompletions) {
return;
}

logger.debug('Execute preview deployment');

const name = `${args.prefix}${args.name}`;
if (name.length > 64) {
logger.error(`Name of the namespace "${name}" is too long (max: 64 characters, is: ${args.name.length})`);
process.exit(ExitCode.error);
return;
}

const api = args.kubeConfig ?
KubernetesApi.fromString(args.kubeConfig.isBase64() ? args.kubeConfig.base64Decode() : args.kubeConfig) :
KubernetesApi.fromDefault();

await createNamespace(api, name);

const files = await async<string>(['**/*.{yml,yaml}'], {
cwd: args.sourceFolder,
});
logger.debug(`Found ${files.length} files for processing.`);

const yamls = (await Promise.all(files.map(async f => await readFile(posix.join(args.sourceFolder, f), 'utf8'))))
.map(yaml => envsubst(yaml))
.join(`${EOL}---${EOL}`);

await kubeConfigCommand.handler({
...args,
configContent: args.kubeConfig,
noInteraction: true,
force: args.ci,
});

logger.debug('Executing <echo "templates" | kubectl -n <name> apply -f ->');
try {
const result = await exec(`echo "${yamls}" | kubectl -n ${name} apply -f -`);
logger.info(result);
} catch (e) {
logger.error(`Error: ${e}`);
process.exit(ExitCode.error);
return;
}

logger.success(`Preview Deployment applied to namespace "${name}".`);
},
};
2 changes: 1 addition & 1 deletion src/kubernetes-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ alias('h', 'help');
help('help');
wrap(terminalWidth());

epilog(chalk.dim(`This tool intends to help with everyday kubernetes administration.`));
epilog(chalk.dim('This tool intends to help with everyday kubernetes administration.'));

const args = parse();
if (args._.length === 0 && !args.getYargsCompletions) {
Expand Down
Loading

0 comments on commit 66d92ad

Please sign in to comment.