Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion src/components/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class Password extends pulumi.ComponentResource {
const optsWithDefauls = pulumi.mergeOptions(opts, {
additionalSecretOutputs: ['value'],
});
super('studion:Password', name, {}, optsWithDefauls);
super('studion:LegacyPassword', name, {}, optsWithDefauls);

this.name = name;
if (args.value) {
Expand Down
69 changes: 69 additions & 0 deletions src/v2/components/password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import * as random from '@pulumi/random';
import { commonTags } from '../../../constants';

export type PasswordArgs = {
value?: pulumi.Input<string>;
};

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

constructor(
name: string,
args: PasswordArgs,
opts: pulumi.ComponentResourceOptions = {},
) {
const optsWithDefauls = pulumi.mergeOptions(opts, {
additionalSecretOutputs: ['value'],
});
super('studion:Password', name, {}, optsWithDefauls);

this.name = name;
if (args.value) {
this.value = pulumi.output(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
110 changes: 110 additions & 0 deletions tests/password/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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';

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

describe('Password component deployment', () => {
const region = process.env.AWS_REGION;
if (!region) {
throw new Error('AWS_REGION environment variable is required');
}

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

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 = `dev/${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',
);
});
});
14 changes: 14 additions & 0 deletions tests/password/infrastructure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { next as studion } from '@studion/infra-code-blocks';

const appName = 'password-test';

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

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

module.exports = {
autoGeneratedPassword,
customPassword,
};
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 {}
2 changes: 1 addition & 1 deletion tests/redis/upstash-redis.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { it } from 'node:test';
import { RedisTestContext } from './test-context';
import assert = require('node:assert');
import * as assert from 'node:assert';
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import Redis from 'ioredis';
import { backOff } from 'exponential-backoff';
Expand Down