Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/v2/components/password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import * as random from '@pulumi/random';
import { commonTags } from '../../../constants';

export namespace Password {
export type Args = {
value?: pulumi.Input<string>;
};
}

export class Password extends pulumi.ComponentResource {
name: string;
value: pulumi.Output<string>;
secret: aws.secretsmanager.Secret;

constructor(
name: string,
args: Password.Args = {},
opts: pulumi.ComponentResourceOptions = {},
) {
super('studion:Password', name, {}, opts);

this.name = name;
if (args.value) {
this.value = pulumi.secret(args.value);
} else {
const password = new random.RandomPassword(
`${this.name}-random-password`,
{
length: 16,
overrideSpecial: '_$',
special: true,
},
{ parent: this },
);
this.value = password.result;
}

this.secret = this.createPasswordSecret(this.value);
this.registerOutputs();
}

private createPasswordSecret(password: pulumi.Input<string>) {
const project = pulumi.getProject();
const stack = pulumi.getStack();

const passwordSecret = new aws.secretsmanager.Secret(
`${this.name}-password-secret`,
{
namePrefix: `${stack}/${project}/${this.name}-`,
tags: commonTags,
},
{ parent: this },
);

const passwordSecretValue = new aws.secretsmanager.SecretVersion(
`${this.name}-password-secret-value`,
{
secretId: passwordSecret.id,
secretString: password,
},
{ parent: this, dependsOn: [passwordSecret] },
);

return passwordSecret;
}
}
1 change: 1 addition & 0 deletions src/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { WebServerLoadBalancer } from './components/web-server/load-balancer';
export { ElastiCacheRedis } from './components/redis/elasticache-redis';
export { UpstashRedis } from './components/redis/upstash-redis';
export { Vpc } from './components/vpc';
export { Password } from './components/password';

import { OtelCollectorBuilder } from './otel/builder';
import { OtelCollector } from './otel';
Expand Down
119 changes: 119 additions & 0 deletions tests/password/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as assert from 'node:assert';
import { InlineProgramArgs } from '@pulumi/pulumi/automation';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import {
DescribeSecretCommand,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
import { PasswordTestContext } from './test-context';
import { after, before, describe, it } from 'node:test';
import * as automation from '../automation';
import { requireEnv } from '../util';

const programArgs: InlineProgramArgs = {
stackName: 'dev',
projectName: 'icb-test-password',
program: () => import('./infrastructure'),
};

const region = requireEnv('AWS_REGION');

const ctx: PasswordTestContext = {
outputs: {},
config: {
autoGeneratedPasswordName: 'password-test-auto',
},
clients: {
secretsManager: new SecretsManagerClient({ region }),
},
};

describe('Password component deployment', () => {
before(async () => {
ctx.outputs = await automation.deploy(programArgs);
});

after(() => automation.destroy(programArgs));

it('should create a password component with the correct configuration', async () => {
const password = ctx.outputs.autoGeneratedPassword.value;

assert.ok(
password.secret,
'Password component should have secret property',
);
assert.ok(password.value, 'Password component should have value property');
assert.strictEqual(
password.name,
ctx.config.autoGeneratedPasswordName,
'Password name should match input',
);
});

it('should create a secret with auto generated password', async () => {
const password = ctx.outputs.autoGeneratedPassword.value;

const secretResult = await ctx.clients.secretsManager.send(
new DescribeSecretCommand({
SecretId: password.secret.arn,
}),
);

assert.ok(secretResult.ARN, 'Secret should exist');
assert.ok(secretResult.Name, 'Secret should have a name');
assert.ok(secretResult.CreatedDate, 'Secret should have creation date');

const expectedPrefix = `${programArgs.stackName}/${programArgs.projectName}/${ctx.config.autoGeneratedPasswordName}-`;
assert.ok(
secretResult.Name?.startsWith(expectedPrefix),
`Secret name should start with ${expectedPrefix}`,
);
});

it('should generate a random password with correct format', async () => {
const password = ctx.outputs.autoGeneratedPassword.value;

const secretValue = await ctx.clients.secretsManager.send(
new GetSecretValueCommand({
SecretId: password.secret.arn,
}),
);

const passwordValue = secretValue.SecretString;
assert.ok(passwordValue, 'Password value should exist');
assert.strictEqual(
passwordValue.length,
16,
'Password should be 16 characters long',
);
assert.ok(secretValue.VersionId, 'Secret should have a version ID');
});

it('should create a secret with custom password value', async () => {
const password = ctx.outputs.customPassword.value;

const secretValue = await ctx.clients.secretsManager.send(
new GetSecretValueCommand({
SecretId: password.secret.arn,
}),
);

const passwordValue = secretValue.SecretString;
assert.strictEqual(
passwordValue,
'customPass!',
'Password should match custom value',
);
});

it('should have password value as a secret', async () => {
const autoGeneratePasswordValue = ctx.outputs.autoGeneratePasswordValue;
assert.ok(
autoGeneratePasswordValue.secret,
'Auto-generated password should be a secret',
);

const customPasswordValue = ctx.outputs.customPasswordValue;
assert.ok(customPasswordValue.secret, 'Custom password should be a secret');
});
});
18 changes: 18 additions & 0 deletions tests/password/infrastructure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { next as studion } from '@studion/infra-code-blocks';

const appName = 'password-test';

const autoGeneratedPassword = new studion.Password(`${appName}-auto`);
const autoGeneratePasswordValue = autoGeneratedPassword.value;

const customPassword = new studion.Password(`${appName}-custom`, {
value: 'customPass!',
});
const customPasswordValue = customPassword.value;

export {
autoGeneratedPassword,
customPassword,
autoGeneratePasswordValue,
customPasswordValue,
};
25 changes: 25 additions & 0 deletions tests/password/test-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { OutputMap } from '@pulumi/pulumi/automation';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';

interface PasswordTestConfig {
autoGeneratedPasswordName: string;
}

interface ConfigContext {
config: PasswordTestConfig;
}

interface PulumiProgramContext {
outputs: OutputMap;
}

interface AwsContext {
clients: {
secretsManager: SecretsManagerClient;
};
}

export interface PasswordTestContext
extends ConfigContext,
PulumiProgramContext,
AwsContext {}