Skip to content

Commit

Permalink
Merge branch 'main' of github.com:aws-amplify/samsara-cli into add-fo…
Browse files Browse the repository at this point in the history
…rm-cli
  • Loading branch information
sdstolworthy committed Sep 26, 2023
2 parents 86d2e87 + 98b1706 commit f4f53f3
Show file tree
Hide file tree
Showing 29 changed files with 868 additions and 40 deletions.
7 changes: 7 additions & 0 deletions .changeset/clever-cats-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@aws-amplify/backend-secret': minor
'@aws-amplify/backend-cli': minor
'@aws-amplify/backend': patch
---

Provides sandbox secret CLI commands
3 changes: 2 additions & 1 deletion 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 packages/backend-secret/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import { UniqueBackendIdentifier } from '@aws-amplify/plugin-types';
export const getSecretClient: (credentialProvider?: AwsCredentialIdentityProvider) => SecretClient;

// @public
export type Secret = {
secretIdentifier: SecretIdentifier;
export type Secret = SecretIdentifier & {
value: string;
};

Expand All @@ -24,7 +23,7 @@ export type SecretAction = 'GET' | 'SET' | 'REMOVE' | 'LIST';

// @public
export type SecretClient = {
getSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier) => Promise<Secret | undefined>;
getSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretIdentifier: SecretIdentifier) => Promise<Secret>;
listSecrets: (backendIdentifier: UniqueBackendIdentifier | BackendId) => Promise<SecretIdentifier[]>;
setSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretName: string, secretValue: string) => Promise<SecretIdentifier>;
removeSecret: (backendIdentifier: UniqueBackendIdentifier | BackendId, secretName: string) => Promise<void>;
Expand Down
7 changes: 2 additions & 5 deletions packages/backend-secret/src/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export type SecretIdentifier = {
/**
* The secret object.
*/
export type Secret = {
secretIdentifier: SecretIdentifier;
value: string;
};
export type Secret = SecretIdentifier & { value: string };

/**
* The client to manage backend secret.
Expand All @@ -30,7 +27,7 @@ export type SecretClient = {
getSecret: (
backendIdentifier: UniqueBackendIdentifier | BackendId,
secretIdentifier: SecretIdentifier
) => Promise<Secret | undefined>;
) => Promise<Secret>;

/**
* List secrets.
Expand Down
24 changes: 21 additions & 3 deletions packages/backend-secret/src/ssm_secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const testSecretIdWithVersion: SecretIdentifier = {
};

const testSecret: Secret = {
secretIdentifier: testSecretIdWithVersion,
...testSecretIdWithVersion,
value: testSecretValue,
};

Expand All @@ -50,7 +50,7 @@ void describe('SSMSecret', () => {
Promise.resolve({
$metadata: {},
Parameter: {
Name: testSecretName,
Name: testBranchSecretFullNamePath,
Value: testSecretValue,
Version: testSecretVersion,
},
Expand Down Expand Up @@ -96,6 +96,25 @@ void describe('SSMSecret', () => {
});
});

void it('gets undefined secret value', async () => {
mock.method(ssmClient, 'getParameter', () =>
Promise.resolve({
$metadata: {},
Parameter: {
Name: testBranchSecretFullNamePath,
Version: testSecretVersion,
},
})
);
const expectedErr = new SecretError(
`The value of secret '${testSecretName}' is undefined`
);
await assert.rejects(
() => ssmSecretClient.getSecret('', { name: testSecretName }),
expectedErr
);
});

void it('throws error', async () => {
const ssmNotFoundException = new ParameterNotFound({
$metadata: {},
Expand All @@ -105,7 +124,6 @@ void describe('SSMSecret', () => {
mock.method(ssmClient, 'getParameter', () =>
Promise.reject(ssmNotFoundException)
);
const ssmSecretClient = new SSMSecretClient(ssmClient);
const expectedErr = SecretError.fromSSMException(ssmNotFoundException);
await assert.rejects(
() => ssmSecretClient.getSecret('', { name: '' }),
Expand Down
24 changes: 15 additions & 9 deletions packages/backend-secret/src/ssm_secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export class SSMSecretClient implements SecretClient {
public getSecret = async (
backendIdentifier: UniqueBackendIdentifier | BackendId,
secretIdentifier: SecretIdentifier
): Promise<Secret | undefined> => {
): Promise<Secret> => {
let secret: Secret | undefined;
const name = this.getParameterFullPath(
backendIdentifier,
secretIdentifier.name
Expand All @@ -96,22 +97,27 @@ export class SSMSecretClient implements SecretClient {
const resp = await this.ssmClient.getParameter({
Name: secretIdentifier.version
? `${name}:${secretIdentifier.version}`
: `${name}`,
: name,
WithDecryption: true,
});
if (resp.Parameter?.Name && resp.Parameter?.Value) {
return {
secretIdentifier: {
name: resp.Parameter?.Name,
version: resp.Parameter?.Version,
},
if (resp.Parameter?.Value) {
secret = {
name: secretIdentifier.name,
version: resp.Parameter?.Version,
value: resp.Parameter?.Value,
};
}
return;
} catch (err) {
throw SecretError.fromSSMException(err as SSMServiceException);
}

if (!secret) {
throw new SecretError(
`The value of secret '${secretIdentifier.name}' is undefined`
);
}

return secret;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const testSecretId: SecretIdentifier = {
};

const testSecret: Secret = {
secretIdentifier: testSecretId,
...testSecretId,
value: testSecretValue,
};

Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"homepage": "https://github.com/aws-amplify/cli#readme",
"dependencies": {
"@aws-amplify/backend-output-schemas": "^0.2.0-alpha.3",
"@aws-amplify/backend-secret": "^0.2.0-alpha.0",
"@aws-amplify/client-config": "^0.2.0-alpha.6",
"@aws-amplify/deployed-backend-client": "^0.2.0-alpha.0",
"@aws-amplify/form-generator": "^0.2.0-alpha.1",
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands/printer/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-console": "off"
}
}
31 changes: 31 additions & 0 deletions packages/cli/src/commands/printer/printer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { EOL } from 'os';

/**
* The class that pretty prints to the console.
*/
export class Printer {
/**
* Print an object/record to console.
*/
static printRecord = <T extends Record<string | number, string | number>>(
object: T
): void => {
let message = '';
const entries = Object.entries(object);
entries.forEach(([key, val]) => {
message = message.concat(` ${key}: ${val}${EOL}`);
});
console.log(message);
};

/**
* Prints an array of objects/records to console.
*/
static printRecords = <T extends Record<string | number, string | number>>(
objects: T[]
): void => {
for (const obj of objects) {
this.printRecord(obj);
}
};
}
15 changes: 14 additions & 1 deletion packages/cli/src/commands/prompter/amplify_prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { confirm } from '@inquirer/prompts';
import { confirm, password } from '@inquirer/prompts';

/**
* Wrapper for prompter library
Expand All @@ -22,4 +22,17 @@ export class AmplifyPrompter {
});
return response;
};

/**
* A secret prompt.
*/
static secretValue = async (
promptMessage = 'Enter secret value'
): Promise<string> => {
return await password({
message: promptMessage,
validate: (val: string) =>
val && val.length > 0 ? true : 'Cannot be empty',
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import assert from 'node:assert';
import { SandboxDeleteCommand } from './sandbox_delete_command.js';
import { SandboxCommand } from '../sandbox_command.js';
import { SandboxSingletonFactory } from '@aws-amplify/sandbox';
import { createSandboxSecretCommand } from '../sandbox-secret/sandbox_secret_command_factory.js';

void describe('sandbox delete command', () => {
let commandRunner: TestCommandRunner;
Expand All @@ -21,11 +22,10 @@ void describe('sandbox delete command', () => {
) as never; // couldn't figure out a good way to type the sandboxDeleteMock so that TS was happy here

const sandboxDeleteCommand = new SandboxDeleteCommand(sandboxFactory);

const sandboxCommand = new SandboxCommand(
sandboxFactory,
sandboxDeleteCommand
);
const sandboxCommand = new SandboxCommand(sandboxFactory, [
sandboxDeleteCommand,
createSandboxSecretCommand(),
]);
const parser = yargs().command(sandboxCommand as unknown as CommandModule);
commandRunner = new TestCommandRunner(parser);
sandboxDeleteMock.mock.resetCalls();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SANDBOX_BRANCH = 'sandbox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it } from 'node:test';
import yargs, { CommandModule } from 'yargs';
import {
TestCommandError,
TestCommandRunner,
} from '../../../test-utils/command_runner.js';
import assert from 'node:assert';
import { SandboxIdResolver } from '../sandbox_id_resolver.js';
import { getSecretClient } from '@aws-amplify/backend-secret';
import { SandboxSecretCommand } from './sandbox_secret_command.js';
import { SandboxSecretGetCommand } from './sandbox_secret_get_command.js';

const testBackendId = 'testBackendId';

void describe('sandbox secret command', () => {
const secretClient = getSecretClient();
const sandboxIdResolver = new SandboxIdResolver({
resolve: () => Promise.resolve(testBackendId),
});

// Creates only a 'get' subcommand.
const sandboxSecretCmd = new SandboxSecretCommand([
new SandboxSecretGetCommand(
sandboxIdResolver,
secretClient
) as unknown as CommandModule,
]);

const parser = yargs().command(sandboxSecretCmd);
const commandRunner = new TestCommandRunner(parser);

void it('show --help', async () => {
const output = await commandRunner.runCommand('secret --help');
assert.match(output, /Manage sandbox secret/);
['secret get'].forEach((cmd) => assert.match(output, new RegExp(cmd)));
['secret set', 'secret list', 'secret remove'].forEach((cmd) =>
assert.doesNotMatch(output, new RegExp(cmd))
);
});

void it('throws error if no verb subcommand', async () => {
await assert.rejects(
() => commandRunner.runCommand('secret'),
(err: TestCommandError) => {
assert.equal(err.error.name, 'YError');
assert.match(err.error.message, /Not enough non-option arguments/);
assert.match(err.output, /Not enough non-option arguments/);
return true;
}
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Argv, CommandModule } from 'yargs';

/**
* Root command to manage sandbox secret.
*/
export class SandboxSecretCommand implements CommandModule<object> {
/**
* @inheritDoc
*/
readonly command: string;

/**
* @inheritDoc
*/
readonly describe: string;

/**
* Root command to manage sandbox secret
*/
constructor(private readonly secretSubCommands: CommandModule[]) {
this.command = 'secret <command>';
this.describe = 'Manage sandbox secret';
}

/**
* @inheritDoc
*/
handler = (): void => {
// no-op for non-terminal command.
return;
};

/**
* @inheritDoc
*/
builder = (yargs: Argv): Argv => {
return (
yargs
.command(this.secretSubCommands)
// Hide inherited options since they are not applicable here.
.option('dirToWatch', {
hidden: true,
})
.option('exclude', {
hidden: true,
})
.option('name', {
hidden: true,
})
.option('out', {
hidden: true,
})
.help()
);
};
}
Loading

0 comments on commit f4f53f3

Please sign in to comment.