Skip to content

Commit

Permalink
WIP: Use pulumi.Input in Helm Chart API
Browse files Browse the repository at this point in the history
  • Loading branch information
hausdorff committed Oct 16, 2018
1 parent c5dd20c commit 54c60f0
Show file tree
Hide file tree
Showing 12 changed files with 764 additions and 588 deletions.
157 changes: 82 additions & 75 deletions pkg/gen/node-templates/helm.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import * as nodepath from "path";

export namespace v2 {
export interface ChartOpts {
repo: string;
chart: string;
version: string;
namespace?: string;
values?: any;
transformations?: ((o: any) => void)[];
fetchOpts?: FetchOpts;
repo: pulumi.Input<string>;
chart: pulumi.Input<string>;
version: pulumi.Input<string>;
namespace?: pulumi.Input<string>;
values?: pulumi.Input<any>;
transformations?: pulumi.Input<((o: any) => void)[]>;
fetchOpts?: pulumi.Input<FetchOpts>;
}

// Chart is a component representing a collection of resources described by an arbitrary Helm
Expand Down Expand Up @@ -49,30 +49,35 @@ export namespace v2 {

try {
// Fetch chart.
fetch(`${config.repo}/${config.chart}`,
{destination: chartDir.name, version: config.version});

// Write overrides file.
const data = JSON.stringify(config.values || {}, undefined, " ");
fs.writeFileSync(overrides.name, data);

// Does not require Tiller. From the `helm template` documentation:
//
// > Render chart templates locally and display the output.
// >
// > This does not require Tiller. However, any values that would normally be
// > looked up or retrieved in-cluster will be faked locally. Additionally, none
// > of the server-side testing of chart validity (e.g. whether an API is supported)
// > is done.
const chart = path.quotePath(nodepath.join(chartDir.name, config.chart));
const release = shell.quote([releaseName]);
const values = path.quotePath(overrides.name);
const defaultValues = path.quotePath(nodepath.join(chartDir.name, config.chart, "values.yaml"));
const namespaceArg = config.namespace ? `--namespace ${shell.quote([config.namespace])}` : "";
const yamlStream = execSync(
`helm template ${chart} --name ${release} --values ${defaultValues} --values ${values} ${namespaceArg}`
).toString();
this.resources = this.parseTemplate(yamlStream, config);
this.resources = pulumi.all([
fetch(`${config.repo}/${config.chart}`,
{destination: chartDir.name, version: config.version}),
config,
]).apply(([_, config]) => {
// Write overrides file.
const data = JSON.stringify(config.values || {}, undefined, " ");
fs.writeFileSync(overrides.name, data);

// Does not require Tiller. From the `helm template` documentation:
//
// > Render chart templates locally and display the output.
// >
// > This does not require Tiller. However, any values that would normally be
// > looked up or retrieved in-cluster will be faked locally. Additionally, none
// > of the server-side testing of chart validity (e.g. whether an API is supported)
// > is done.
const chart = path.quotePath(nodepath.join(chartDir.name, config.chart));
const release = shell.quote([releaseName]);
const values = path.quotePath(overrides.name);
const defaultValues = path.quotePath(nodepath.join(chartDir.name, config.chart, "values.yaml"));
const namespaceArg = config.namespace ? `--namespace ${shell.quote([config.namespace])}` : "";
const yamlStream = execSync(
`helm template ${chart} --name ${release} --values ${defaultValues} --values ${values} ${namespaceArg}`
).toString();
// TODO: Bug requiring cast fixed in pulumi/pulumi#2061. Remove in next release
// of pulumi/pulumi.
return this.parseTemplate(yamlStream, <any>config.transformations);
});
} catch (e) {
// Shed stack trace, only emit the error.
throw new pulumi.RunError(e.toString());
Expand All @@ -85,16 +90,16 @@ export namespace v2 {

parseTemplate(
yamlStream: string,
config: ChartOpts
): { [key: string]: pulumi.CustomResource } {
transforms?: ((o: any) => void)[],
): pulumi.Output<{ [key: string]: pulumi.CustomResource }> {
const objs = jsyaml
.safeLoadAll(yamlStream)
.filter(a => a != null && "kind" in a)
.sort(helmSort);
return k8s.yaml.parse(
{
yaml: objs.map(o => jsyaml.safeDump(o)),
transformations: config.transformations || []
transformations: transforms || []
},
{ parent: this }
);
Expand Down Expand Up @@ -162,52 +167,52 @@ export namespace v2 {

export interface FetchOpts {
// Specific version of a chart. Without this, the latest version is fetched.
version?: string;
version?: pulumi.Input<string>;
// Verify certificates of HTTPS-enabled servers using this CA bundle.
caFile?: string;
caFile?: pulumi.Input<string>;
// Identify HTTPS client using this SSL certificate file.
certFile?: string;
certFile?: pulumi.Input<string>;
// Identify HTTPS client using this SSL key file.
keyFile?: string;
keyFile?: pulumi.Input<string>;
// Location to write the chart. If this and tardir are specified, tardir is appended to this
// (default ".").
destination?: string;
destination?: pulumi.Input<string>;
// Keyring containing public keys (default "/Users/alex/.gnupg/pubring.gpg").
keyring?: string;
keyring?: pulumi.Input<string>;
// Chart repository password.
password?: string;
password?: pulumi.Input<string>;
// Chart repository url where to locate the requested chart.
repo?: string;
repo?: pulumi.Input<string>;
// If untar is specified, this flag specifies the name of the directory into which the chart is
// expanded (default ".").
untardir?: string;
untardir?: pulumi.Input<string>;
// Chart repository username.
username?: string;
username?: pulumi.Input<string>;
// Location of your Helm config. Overrides $HELM_HOME (default "/Users/alex/.helm").
home?: string;
home?: pulumi.Input<string>;
// Use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is
// ignored.
devel?: boolean;
devel?: pulumi.Input<boolean>;
// Fetch the provenance file, but don't perform verification.
prov?: boolean;
prov?: pulumi.Input<boolean>;
// If set to false, will leave the chart as a tarball after downloading.
untar?: boolean;
untar?: pulumi.Input<boolean>;
// Verify the package against its signature.
verify?: boolean;
verify?: pulumi.Input<boolean>;
}
// Retrieve a package from a package repository, and download it locally.
Expand All @@ -221,29 +226,31 @@ export interface FetchOpts {
// If the `verify` option is specified, the requested chart MUST have a provenance file, and MUST
// pass the verification process. Failure in any part of this will result in an error, and the chart
// will not be saved locally.
export function fetch(chart: string, opts?: FetchOpts) {
const flags: string[] = [];
if (opts !== undefined) {
// Untar by default.
if(opts.untar !== false) { flags.push(`--untar`); }
// For arguments that are not paths to files, it is sufficent to use shell.quote to quote the arguments.
// However, for arguments that are actual paths to files we use path.quotePath (note that path here is
// not the node path builtin module). This ensures proper escaping of paths on Windows.
if (opts.version !== undefined) { flags.push(`--version ${shell.quote([opts.version])}`); }
if (opts.caFile !== undefined) { flags.push(`--ca-file ${path.quotePath(opts.caFile)}`); }
if (opts.certFile !== undefined) { flags.push(`--cert-file ${path.quotePath(opts.certFile)}`); }
if (opts.keyFile !== undefined) { flags.push(`--key-file ${path.quotePath(opts.keyFile)}`); }
if (opts.destination !== undefined) { flags.push(`--destination ${path.quotePath(opts.destination)}`); }
if (opts.keyring !== undefined) { flags.push(`--keyring ${path.quotePath(opts.keyring)}`); }
if (opts.password !== undefined) { flags.push(`--password ${shell.quote([opts.password])}`); }
if (opts.repo !== undefined) { flags.push(`--repo ${shell.quote([opts.repo])}`); }
if (opts.untardir !== undefined) { flags.push(`--untardir ${path.quotePath(opts.untardir)}`); }
if (opts.username !== undefined) { flags.push(`--username ${shell.quote([opts.username])}`); }
if (opts.home !== undefined) { flags.push(`--home ${path.quotePath(opts.home)}`); }
if (opts.devel === true) { flags.push(`--devel`); }
if (opts.prov === true) { flags.push(`--prov`); }
if (opts.verify === true) { flags.push(`--verify`); }
}
execSync(`helm fetch ${shell.quote([chart])} ${flags.join(" ")}`);
export function fetch(chart: string, opts?: FetchOpts): pulumi.Output<void> {
return pulumi.output(opts).apply(opts => {
const flags: string[] = [];
if (opts !== undefined) {
// Untar by default.
if(opts.untar !== false) { flags.push(`--untar`); }
// For arguments that are not paths to files, it is sufficent to use shell.quote to quote the arguments.
// However, for arguments that are actual paths to files we use path.quotePath (note that path here is
// not the node path builtin module). This ensures proper escaping of paths on Windows.
if (opts.version !== undefined) { flags.push(`--version ${shell.quote([opts.version])}`); }
if (opts.caFile !== undefined) { flags.push(`--ca-file ${path.quotePath(opts.caFile)}`); }
if (opts.certFile !== undefined) { flags.push(`--cert-file ${path.quotePath(opts.certFile)}`); }
if (opts.keyFile !== undefined) { flags.push(`--key-file ${path.quotePath(opts.keyFile)}`); }
if (opts.destination !== undefined) { flags.push(`--destination ${path.quotePath(opts.destination)}`); }
if (opts.keyring !== undefined) { flags.push(`--keyring ${path.quotePath(opts.keyring)}`); }
if (opts.password !== undefined) { flags.push(`--password ${shell.quote([opts.password])}`); }
if (opts.repo !== undefined) { flags.push(`--repo ${shell.quote([opts.repo])}`); }
if (opts.untardir !== undefined) { flags.push(`--untardir ${path.quotePath(opts.untardir)}`); }
if (opts.username !== undefined) { flags.push(`--username ${shell.quote([opts.username])}`); }
if (opts.home !== undefined) { flags.push(`--home ${path.quotePath(opts.home)}`); }
if (opts.devel === true) { flags.push(`--devel`); }
if (opts.prov === true) { flags.push(`--prov`); }
if (opts.verify === true) { flags.push(`--verify`); }
}
execSync(`helm fetch ${shell.quote([chart])} ${flags.join(" ")}`);
});
}
2 changes: 1 addition & 1 deletion pkg/gen/node-templates/package.json.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "tsc"
},
"dependencies": {
"@pulumi/pulumi": "^0.15.1",
"@pulumi/pulumi": "^0.16.0",
"@types/js-yaml": "^3.11.2",
"js-yaml": "^3.12.0",
"shell-quote": "^1.6.1",
Expand Down
90 changes: 48 additions & 42 deletions pkg/gen/node-templates/provider.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export namespace yaml {

export function parse(
config: ConfigGroupOpts, opts?: pulumi.CustomResourceOptions
): {[key: string]: pulumi.CustomResource} {
let resources: {[key: string]: pulumi.CustomResource} = {};
): pulumi.Output<{[key: string]: pulumi.CustomResource}> {
// Parse all YAML config files.
let configFileResources: pulumi.Output<{[key: string]: pulumi.CustomResource}>[] = [];
if (config.files !== undefined) {
let files: string[] = [];
if (typeof config.files === 'string') {
Expand All @@ -42,43 +43,48 @@ export namespace yaml {
}
}

for (const file of files) {
const cf = new ConfigFile(file,
{file: file, transformations: config.transformations}, opts);
if (cf != null) {
resources = {...resources, ...cf.resources}
}
}
configFileResources = files.map(file =>
new ConfigFile(file, {file: file, transformations: config.transformations}, opts)
.resources);
}

if (config.yaml !== undefined) {
let yamlTexts: string[] = [];
if (typeof config.yaml === 'string') {
yamlTexts.push(config.yaml);
} else {
yamlTexts.push(...config.yaml);
}
return pulumi.output(configFileResources)
.apply(configFileResources => {
let resources: {[key: string]: pulumi.CustomResource} = {};

for (const text of yamlTexts) {
const objs = jsyaml.safeLoadAll(text);
const docResources = parseYamlDocument(
{objs: objs, transformations: config.transformations}, opts);
resources = {...resources, ...docResources};
}
}
for (const cf of configFileResources) {
resources = {...resources, ...cf}
}

if (config.objs !== undefined) {
const objs= Array.isArray(config.objs) ? config.objs: [config.objs];
const docResources = parseYamlDocument(
{objs: objs, transformations: config.transformations}, opts);
resources = {...resources, ...docResources};
}
if (config.yaml !== undefined) {
let yamlTexts: string[] = [];
if (typeof config.yaml === 'string') {
yamlTexts.push(config.yaml);
} else {
yamlTexts.push(...config.yaml);
}


for (const text of yamlTexts) {
const objs = jsyaml.safeLoadAll(text);
const docResources = parseYamlDocument(
{objs: objs, transformations: config.transformations}, opts);
resources = {...resources, ...docResources};
}
}

return resources;
}
if (config.objs !== undefined) {
const objs= Array.isArray(config.objs) ? config.objs: [config.objs];
const docResources = parseYamlDocument(
{objs: objs, transformations: config.transformations}, opts);
resources = {...resources, ...docResources};
}

return resources;
});
}
export abstract class CollectionComponentResource extends pulumi.ComponentResource {
resources: { [key: string]: pulumi.CustomResource };
resources: pulumi.Output<{ [key: string]: pulumi.CustomResource }>;

constructor(
resourceType: string, name: string, config: any, opts?: pulumi.ComponentResourceOptions,
Expand All @@ -95,12 +101,12 @@ export namespace yaml {
{{#Groups}}
{{#Versions}}
{{#Kinds}}
public getResource(groupVersionKind: "{{RawAPIVersion}}/{{Kind}}", name: string): {{Group}}.{{Version}}.{{Kind}};
public getResource(groupVersionKind: "{{RawAPIVersion}}/{{Kind}}", namespace: string, name: string): {{Group}}.{{Version}}.{{Kind}};
public getResource(groupVersionKind: "{{RawAPIVersion}}/{{Kind}}", name: string): pulumi.Output<{{Group}}.{{Version}}.{{Kind}}>;
public getResource(groupVersionKind: "{{RawAPIVersion}}/{{Kind}}", namespace: string, name: string): pulumi.Output<{{Group}}.{{Version}}.{{Kind}}>;
{{/Kinds}}
{{/Versions}}
{{/Groups}}
public getResource(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.CustomResource {
public getResource(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.Output<pulumi.CustomResource> {
return this.getResourceImpl(groupVersionKind, namespaceOrName, name);
}

Expand All @@ -110,20 +116,20 @@ export namespace yaml {
* For example:
* getCustomResource("monitoring.coreos.com/v1/ServiceMonitor", "kube-prometheus-exporter-kubernetes")
*/
public getCustomResource<T extends pulumi.CustomResource>(groupVersionKind: string, namespace: string): T;
public getCustomResource<T extends pulumi.CustomResource>(groupVersionKind: string, namespace: string, name: string): T;
public getCustomResource(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.CustomResource {
public getCustomResource<T extends pulumi.CustomResource>(groupVersionKind: string, namespace: string): pulumi.Output<T>;
public getCustomResource<T extends pulumi.CustomResource>(groupVersionKind: string, namespace: string, name: string): pulumi.Output<T>;
public getCustomResource(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.Output<pulumi.CustomResource> {
return this.getResourceImpl(groupVersionKind, namespaceOrName, name);
}

private getResourceImpl(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.CustomResource {
private getResourceImpl(groupVersionKind: string, namespaceOrName: string, name?: string): pulumi.Output<pulumi.CustomResource> {
// `id` will either be `${name}` or `${namespace}/${name}`.
let id = namespaceOrName;
if (name !== undefined) {
id = `${namespaceOrName}/${name}`;
}

return this.resources[`${groupVersionKind}::${id}`];
return this.resources.apply(r => r[`${groupVersionKind}::${id}`]);
}
}

Expand Down Expand Up @@ -159,10 +165,10 @@ export namespace yaml {
super("kubernetes:yaml:ConfigFile", name, config, opts);
const text = fs.readFileSync(config && config.file || name).toString();
const objs = jsyaml.safeLoadAll(text);
this.resources = parseYamlDocument({
this.resources = pulumi.output(parseYamlDocument({
objs: objs,
transformations: config && config.transformations || []
}, {parent: this});
}, {parent: this}));
}
}

Expand Down
Loading

0 comments on commit 54c60f0

Please sign in to comment.