Skip to content
This repository was archived by the owner on Feb 13, 2024. It is now read-only.

Commit 6dd050e

Browse files
authored
Install command fixes (#36)
1 parent 3f59e18 commit 6dd050e

File tree

10 files changed

+138
-47
lines changed

10 files changed

+138
-47
lines changed

src/commands/install.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as shell from '../utils/shell';
99
import { cantHappen } from '../utils/never';
1010
import { promptBundle, BundleSelection, fileBundleSelection, repoBundleSelection } from '../utils/bundleselection';
1111
import { promptForParameters } from '../utils/parameters';
12-
import { withOptionalTempFile } from '../utils/tempfile';
12+
import { promptForCredentials } from '../utils/credentials';
1313

1414
export async function install(target?: any): Promise<void> {
1515
if (!target) {
@@ -52,12 +52,17 @@ async function installCore(bundlePick: BundleSelection): Promise<void> {
5252
return;
5353
}
5454

55+
const credentialSet = await promptForCredentials(bundlePick, shell.shell, 'Credential set to install bundle with');
56+
if (credentialSet.cancelled) {
57+
return;
58+
}
59+
5560
const parameterValues = await promptForParameters(bundlePick, 'Install', 'Enter installation parameters');
5661
if (parameterValues.cancelled) {
5762
return;
5863
}
5964

60-
const installResult = await installToViaTempFile(bundlePick, name, parameterValues.values);
65+
const installResult = await installTo(bundlePick, name, parameterValues.value, credentialSet.value);
6166

6267
if (succeeded(installResult)) {
6368
await refreshBundleExplorer();
@@ -66,22 +71,17 @@ async function installCore(bundlePick: BundleSelection): Promise<void> {
6671
await showDuffleResult('install', (bundleId) => bundleId, installResult);
6772
}
6873

69-
async function installToViaTempFile(bundlePick: BundleSelection, name: string, parameterValues: any): Promise<Errorable<string>> {
70-
const parametersJSON = parameterValues ? JSON.stringify(parameterValues, undefined, 2) : undefined;
71-
return withOptionalTempFile(parametersJSON, 'json', (paramsFile) => installTo(bundlePick, name, paramsFile));
72-
}
73-
74-
async function installTo(bundlePick: BundleSelection, name: string, paramsFile: string | undefined): Promise<Errorable<string>> {
74+
async function installTo(bundlePick: BundleSelection, name: string, params: { [key: string]: string }, credentialSet: string | undefined): Promise<Errorable<string>> {
7575
if (bundlePick.kind === 'folder') {
7676
const folderPath = bundlePick.path;
7777
const bundlePath = path.join(folderPath, "cnab", "bundle.json");
7878
const installResult = await longRunning(`Duffle installing ${bundlePath}`,
79-
() => duffle.installFile(shell.shell, bundlePath, name, paramsFile)
79+
() => duffle.installFile(shell.shell, bundlePath, name, params, credentialSet)
8080
);
8181
return map(installResult, (_) => bundlePath);
8282
} else if (bundlePick.kind === 'repo') {
8383
const installResult = await longRunning(`Duffle installing ${bundlePick.bundle}`,
84-
() => duffle.installBundle(shell.shell, bundlePick.label /* because bundlePick.bundle doesn't work */, name, paramsFile)
84+
() => duffle.installBundle(shell.shell, bundlePick.bundle, name, params, credentialSet)
8585
);
8686
return map(installResult, (_) => bundlePick.bundle);
8787
}

src/duffle/duffle.paths.ts

+15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export function credentialSetPath(name: string): string {
44
return path.join(credentialSetDir(), name + '.yaml');
55
}
66

7+
export function repoBundlePath(bundleRef: string): string {
8+
return path.join(repositoriesDir(), localPath(bundleRef) + '.json');
9+
}
10+
711
function home(): string {
812
const envHome = process.env['DUFFLE_HOME'];
913
if (envHome) {
@@ -25,3 +29,14 @@ function osHome(): string {
2529
function credentialSetDir(): string {
2630
return path.join(home(), 'credentials');
2731
}
32+
33+
function repositoriesDir(): string {
34+
return path.join(home(), 'repositories');
35+
}
36+
37+
function localPath(bundleRef: string): string {
38+
const bits = bundleRef.split('/');
39+
const last = bits.pop()!;
40+
bits.push('bundles', last);
41+
return bits.join('/');
42+
}

src/duffle/duffle.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Errorable } from '../utils/errorable';
77
import * as shell from '../utils/shell';
88
import { RepoBundle } from './duffle.objectmodel';
99
import { sharedTerminal } from './sharedterminal';
10+
import * as pairs from '../utils/pairs';
1011

1112
const logChannel = vscode.window.createOutputChannel("Duffle");
1213

@@ -97,16 +98,26 @@ export async function build(sh: shell.Shell, folderPath: string): Promise<Errora
9798
return await invokeObj(sh, 'build', '.', { cwd: folderPath }, (s) => null);
9899
}
99100

100-
export async function installFile(sh: shell.Shell, bundleFilePath: string, name: string, paramsFile: string | undefined): Promise<Errorable<null>> {
101-
return await invokeObj(sh, 'install', `${name} -f "${bundleFilePath}" ${paramsArg(paramsFile)}`, {}, (s) => null);
101+
export async function installFile(sh: shell.Shell, bundleFilePath: string, name: string, params: { [key: string]: string }, credentialSet: string | undefined): Promise<Errorable<null>> {
102+
return await invokeObj(sh, 'install', `${name} -f "${bundleFilePath}" ${paramsArgs(params)} ${credentialArg(credentialSet)}`, {}, (s) => null);
102103
}
103104

104-
export async function installBundle(sh: shell.Shell, bundleName: string, name: string, paramsFile: string | undefined): Promise<Errorable<null>> {
105-
return await invokeObj(sh, 'install', `${name} ${bundleName} ${paramsArg(paramsFile)}`, {}, (s) => null);
105+
export async function installBundle(sh: shell.Shell, bundleName: string, name: string, params: { [key: string]: string }, credentialSet: string | undefined): Promise<Errorable<null>> {
106+
return await invokeObj(sh, 'install', `${name} ${bundleName} ${paramsArgs(params)} ${credentialArg(credentialSet)}`, {}, (s) => null);
106107
}
107108

108-
function paramsArg(file: string | undefined): string {
109-
return file ? `-p "${file}"` : '';
109+
function paramsArgs(parameters: { [key: string]: string }): string {
110+
return pairs.fromStringMap(parameters)
111+
.filter((p) => !!p.value)
112+
.map((p) => `--set ${p.key}=${shell.safeValue(p.value)}`)
113+
.join(' ');
114+
}
115+
116+
function credentialArg(credentialSet: string | undefined): string {
117+
if (credentialSet) {
118+
return `-c ${credentialSet}`;
119+
}
120+
return '';
110121
}
111122

112123
function fromHeaderedTable<T>(lines: string[]): T[] {

src/utils/bundleselection.ts

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as path from 'path';
33

44
import { selectQuickPick } from './host';
55
import { RepoBundle } from '../duffle/duffle.objectmodel';
6+
import * as dufflepaths from '../duffle/duffle.paths';
7+
import { cantHappen } from './never';
68

79
export interface FolderBundleSelection {
810
readonly kind: 'folder';
@@ -51,3 +53,12 @@ export function repoBundleSelection(bundle: RepoBundle): BundleSelection {
5153
bundle: `${bundle.repository}/${bundle.name}`
5254
};
5355
}
56+
57+
export function bundleJSONPath(bundlePick: BundleSelection) {
58+
if (bundlePick.kind === "folder") {
59+
return path.join(bundlePick.path, "bundle.json");
60+
} else if (bundlePick.kind === "repo") {
61+
return dufflepaths.repoBundlePath(bundlePick.bundle);
62+
}
63+
return cantHappen(bundlePick);
64+
}

src/utils/cancellable.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface Accepted<T> {
2+
readonly cancelled: false;
3+
readonly value: T;
4+
}
5+
6+
interface Cancelled {
7+
readonly cancelled: true;
8+
}
9+
10+
export type Cancellable<T> = Accepted<T> | Cancelled;

src/utils/credentials.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as vscode from 'vscode';
2+
3+
import * as duffle from '../duffle/duffle';
4+
import { Cancellable } from './cancellable';
5+
import { Shell } from './shell';
6+
import { failed } from './errorable';
7+
import { BundleSelection, bundleJSONPath } from './bundleselection';
8+
import { fs } from './fs';
9+
10+
export async function promptForCredentials(bundlePick: BundleSelection, sh: Shell, prompt: string): Promise<Cancellable<string | undefined>> {
11+
if (!(await bundleHasCredentials(bundlePick))) {
12+
return { cancelled: false, value: undefined };
13+
}
14+
15+
const credentialSets = await duffle.listCredentialSets(sh);
16+
if (failed(credentialSets)) {
17+
// Fall back to making the user type it in unaided
18+
const credentialSet = await vscode.window.showInputBox({ prompt: prompt });
19+
if (!credentialSet) {
20+
return { cancelled: true };
21+
}
22+
return { cancelled: false, value: credentialSet };
23+
}
24+
25+
const credentialSet = await vscode.window.showQuickPick(credentialSets.result, { placeHolder: prompt });
26+
if (!credentialSet) {
27+
return { cancelled: true };
28+
}
29+
30+
return { cancelled: false, value: credentialSet };
31+
}
32+
33+
// TODO: deduplicate with parameters parser
34+
async function bundleHasCredentials(bundlePick: BundleSelection): Promise<boolean> {
35+
const jsonPath = bundleJSONPath(bundlePick);
36+
return await parseHasCredentialsFromJSONFile(jsonPath);
37+
}
38+
39+
async function parseHasCredentialsFromJSONFile(jsonFile: string): Promise<boolean> {
40+
const json = await fs.readFile(jsonFile, 'utf8');
41+
const credentials = JSON.parse(json).credentials;
42+
return (credentials && Object.keys(credentials).length > 0);
43+
}

src/utils/dialog.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22

33
export const END_DIALOG_FN = "endDialog()";
44

5-
export function dialog(tabTitle: string, htmlBody: string, formId: string): Promise<any> {
5+
export function dialog(tabTitle: string, htmlBody: string, formId: string): Promise<{ [key: string]: string }> {
66
return new Promise<any>((resolve, reject) => {
77
const postbackScript = `<script>
88
function ${END_DIALOG_FN} {

src/utils/pairs.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { iter } from "./iterable";
2+
3+
export interface Pair {
4+
readonly key: string;
5+
readonly value: string;
6+
}
7+
8+
export function fromStringMap(source: { [key: string]: string }): Pair[] {
9+
return iter(fromStringMapCore(source)).toArray();
10+
}
11+
12+
function* fromStringMapCore(source: { [key: string]: string }): IterableIterator<Pair> {
13+
for (const k in source) {
14+
yield { key: k, value: source[k] };
15+
}
16+
}

src/utils/parameters.ts

+8-30
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
1-
import * as path from 'path';
2-
31
import { fs } from './fs';
42
import { ParameterDefinition } from "../duffle/duffle.objectmodel";
5-
import { BundleSelection } from "./bundleselection";
3+
import { BundleSelection, bundleJSONPath } from "./bundleselection";
64
import { END_DIALOG_FN, dialog } from "./dialog";
7-
import { cantHappen } from './never';
8-
9-
interface ParameterValues {
10-
readonly cancelled: false;
11-
readonly values: any;
12-
}
13-
14-
interface Cancelled {
15-
readonly cancelled: true;
16-
}
5+
import { Cancellable } from './cancellable';
176

18-
export type ParameterValuesPromptResult = ParameterValues | Cancelled;
7+
export type ParameterValuesPromptResult = Cancellable<{ [key: string]: string }>;
198

209
export async function promptForParameters(bundlePick: BundleSelection, actionName: string, prompt: string): Promise<ParameterValuesPromptResult> {
2110
const definitions = await bundleParameters(bundlePick);
2211
if (!definitions || definitions.length === 0) {
23-
return { cancelled: false, values: {} };
12+
return { cancelled: false, value: {} };
2413
}
2514

2615
const parameterFormId = 'pvform';
@@ -36,7 +25,7 @@ export async function promptForParameters(bundlePick: BundleSelection, actionNam
3625
return { cancelled: true };
3726
}
3827

39-
return { cancelled: false, values: parameterValues };
28+
return { cancelled: false, value: parameterValues };
4029
}
4130

4231
function parameterEntryTable(ps: ParameterDefinition[]): string {
@@ -56,7 +45,7 @@ function parameterEntryRow(p: ParameterDefinition): string {
5645
}
5746

5847
function inputWidget(p: ParameterDefinition): string {
59-
if (p.type === "boolean") {
48+
if (p.type === "bool") {
6049
return `<select name="${p.name}"><option>True</option><option>False</option></select>`;
6150
}
6251
if (p.allowedValues) {
@@ -67,20 +56,9 @@ function inputWidget(p: ParameterDefinition): string {
6756
return `<input name="${p.name}" type="text" value="${defval}" />`;
6857
}
6958

70-
function localPath(bundleRef: string): string {
71-
const bits = bundleRef.split('/');
72-
const last = bits.pop()!;
73-
bits.push('bundles', last);
74-
return bits.join('/');
75-
}
76-
7759
async function bundleParameters(bundlePick: BundleSelection): Promise<ParameterDefinition[]> {
78-
if (bundlePick.kind === "folder") {
79-
return await parseParametersFromJSONFile(path.join(bundlePick.path, "bundle.json"));
80-
} else if (bundlePick.kind === "repo") {
81-
return await parseParametersFromJSONFile(path.join(process.env["USERPROFILE"]!, ".duffle", "repositories", localPath(bundlePick.bundle) + '.json'));
82-
}
83-
return cantHappen(bundlePick);
60+
const jsonPath = bundleJSONPath(bundlePick);
61+
return await parseParametersFromJSONFile(jsonPath);
8462
}
8563

8664
async function parseParametersFromJSONFile(jsonFile: string): Promise<ParameterDefinition[]> {

src/utils/shell.ts

+7
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,10 @@ function pathVariableName(env: any): string {
179179
function pathEntrySeparator() {
180180
return isWindows() ? ';' : ':';
181181
}
182+
183+
export function safeValue(s: string): string {
184+
if (s.indexOf(' ') >= 0) {
185+
return `"${s}"`; // TODO: confirm quoting style on Mac/Linux
186+
}
187+
return s;
188+
}

0 commit comments

Comments
 (0)