From 95ef3fea825e0dfc3a0c9c9f144c114358d44403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Thu, 4 Dec 2025 13:18:22 +0100 Subject: [PATCH 01/14] Update redis component wrong imports --- tests/redis/upstash-redis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redis/upstash-redis.test.ts b/tests/redis/upstash-redis.test.ts index f4f66c1..e04ae4c 100644 --- a/tests/redis/upstash-redis.test.ts +++ b/tests/redis/upstash-redis.test.ts @@ -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'; From cf9f385231d48b8bc73d2b6af6ce0e075c918488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Thu, 4 Dec 2025 13:18:49 +0100 Subject: [PATCH 02/14] Move password component to v2 folder --- src/v2/components/password/index.ts | 69 +++++++++++++++++++++++++++++ src/v2/index.ts | 1 + 2 files changed, 70 insertions(+) create mode 100644 src/v2/components/password/index.ts diff --git a/src/v2/components/password/index.ts b/src/v2/components/password/index.ts new file mode 100644 index 0000000..5a8abf7 --- /dev/null +++ b/src/v2/components/password/index.ts @@ -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; +}; + +export class Password extends pulumi.ComponentResource { + name: string; + value: pulumi.Output; + 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) { + 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; + } +} diff --git a/src/v2/index.ts b/src/v2/index.ts index aaaabc7..807116b 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -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'; From ca3548c2ea1f3b00dcb24c717390469024b55b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Thu, 4 Dec 2025 13:19:05 +0100 Subject: [PATCH 03/14] Implement password component tests --- tests/password/index.test.ts | 118 +++++++++++++++++++++++++ tests/password/infrastructure/index.ts | 14 +++ tests/password/test-context.ts | 33 +++++++ 3 files changed, 165 insertions(+) create mode 100644 tests/password/index.test.ts create mode 100644 tests/password/infrastructure/index.ts create mode 100644 tests/password/test-context.ts diff --git a/tests/password/index.test.ts b/tests/password/index.test.ts new file mode 100644 index 0000000..07f10e0 --- /dev/null +++ b/tests/password/index.test.ts @@ -0,0 +1,118 @@ +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', + customPasswordName: 'password-test-custom', + exponentialBackOffConfig: { + delayFirstAttempt: true, + numOfAttempts: 5, + startingDelay: 2000, + timeMultiple: 2, + jitter: 'full', + }, + }, + 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', + ); + }); +}); diff --git a/tests/password/infrastructure/index.ts b/tests/password/infrastructure/index.ts new file mode 100644 index 0000000..b32afcb --- /dev/null +++ b/tests/password/infrastructure/index.ts @@ -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, +}; diff --git a/tests/password/test-context.ts b/tests/password/test-context.ts new file mode 100644 index 0000000..1d15da0 --- /dev/null +++ b/tests/password/test-context.ts @@ -0,0 +1,33 @@ +import { OutputMap } from '@pulumi/pulumi/automation'; +import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; + +interface PasswordTestConfig { + autoGeneratedPasswordName: string; + customPasswordName: string; + exponentialBackOffConfig: { + delayFirstAttempt: boolean; + numOfAttempts: number; + startingDelay: number; + timeMultiple: number; + jitter: 'full' | 'none'; + }; +} + +interface ConfigContext { + config: PasswordTestConfig; +} + +interface PulumiProgramContext { + outputs: OutputMap; +} + +interface AwsContext { + clients: { + secretsManager: SecretsManagerClient; + }; +} + +export interface PasswordTestContext + extends ConfigContext, + PulumiProgramContext, + AwsContext {} From 410b3e447141bbddd95de7758a962fc1de8fc447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 5 Dec 2025 12:23:46 +0100 Subject: [PATCH 04/14] Remove backOff config --- tests/password/index.test.ts | 8 -------- tests/password/test-context.ts | 8 -------- 2 files changed, 16 deletions(-) diff --git a/tests/password/index.test.ts b/tests/password/index.test.ts index 07f10e0..cb9e2b9 100644 --- a/tests/password/index.test.ts +++ b/tests/password/index.test.ts @@ -25,14 +25,6 @@ describe('Password component deployment', () => { outputs: {}, config: { autoGeneratedPasswordName: 'password-test-auto', - customPasswordName: 'password-test-custom', - exponentialBackOffConfig: { - delayFirstAttempt: true, - numOfAttempts: 5, - startingDelay: 2000, - timeMultiple: 2, - jitter: 'full', - }, }, clients: { secretsManager: new SecretsManagerClient({ region }), diff --git a/tests/password/test-context.ts b/tests/password/test-context.ts index 1d15da0..7607393 100644 --- a/tests/password/test-context.ts +++ b/tests/password/test-context.ts @@ -3,14 +3,6 @@ import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; interface PasswordTestConfig { autoGeneratedPasswordName: string; - customPasswordName: string; - exponentialBackOffConfig: { - delayFirstAttempt: boolean; - numOfAttempts: number; - startingDelay: number; - timeMultiple: number; - jitter: 'full' | 'none'; - }; } interface ConfigContext { From 23e3e5d942d2db87799a4c1643f0efc90ee32781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 5 Dec 2025 12:24:22 +0100 Subject: [PATCH 05/14] Add legacy prefix to password v1 component --- src/components/password.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/password.ts b/src/components/password.ts index 3b3c7e5..22e8373 100644 --- a/src/components/password.ts +++ b/src/components/password.ts @@ -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) { From 03738279c5d5f42459b5d242ff64953452f87a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 12 Dec 2025 12:02:31 +0100 Subject: [PATCH 06/14] Remove legacy prefix from v1 component --- src/components/password.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/password.ts b/src/components/password.ts index 22e8373..3b3c7e5 100644 --- a/src/components/password.ts +++ b/src/components/password.ts @@ -20,7 +20,7 @@ export class Password extends pulumi.ComponentResource { const optsWithDefauls = pulumi.mergeOptions(opts, { additionalSecretOutputs: ['value'], }); - super('studion:LegacyPassword', name, {}, optsWithDefauls); + super('studion:Password', name, {}, optsWithDefauls); this.name = name; if (args.value) { From 96b281a83c90054de16127ce3bb20adaefb615b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 12 Dec 2025 12:04:48 +0100 Subject: [PATCH 07/14] Add password namespace --- src/v2/components/password/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/v2/components/password/index.ts b/src/v2/components/password/index.ts index 5a8abf7..8437967 100644 --- a/src/v2/components/password/index.ts +++ b/src/v2/components/password/index.ts @@ -3,9 +3,11 @@ import * as pulumi from '@pulumi/pulumi'; import * as random from '@pulumi/random'; import { commonTags } from '../../../constants'; -export type PasswordArgs = { - value?: pulumi.Input; -}; +export namespace Password { + export type Args = { + value?: pulumi.Input; + }; +} export class Password extends pulumi.ComponentResource { name: string; @@ -14,7 +16,7 @@ export class Password extends pulumi.ComponentResource { constructor( name: string, - args: PasswordArgs, + args: Password.Args, opts: pulumi.ComponentResourceOptions = {}, ) { const optsWithDefauls = pulumi.mergeOptions(opts, { From e19018fb1273fd7e2f45bd023a17086c0a97b193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 12 Dec 2025 12:05:23 +0100 Subject: [PATCH 08/14] Update imports --- tests/redis/upstash-redis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redis/upstash-redis.test.ts b/tests/redis/upstash-redis.test.ts index e04ae4c..f4f66c1 100644 --- a/tests/redis/upstash-redis.test.ts +++ b/tests/redis/upstash-redis.test.ts @@ -1,6 +1,6 @@ import { it } from 'node:test'; import { RedisTestContext } from './test-context'; -import * as assert from 'node:assert'; +import assert = require('node:assert'); import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; import Redis from 'ioredis'; import { backOff } from 'exponential-backoff'; From 201983d6e4145d369ee2b35bad1ecdec77753035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 12 Dec 2025 12:09:04 +0100 Subject: [PATCH 09/14] Make password args optional --- src/v2/components/password/index.ts | 2 +- tests/password/infrastructure/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/components/password/index.ts b/src/v2/components/password/index.ts index 8437967..1dd5e34 100644 --- a/src/v2/components/password/index.ts +++ b/src/v2/components/password/index.ts @@ -16,7 +16,7 @@ export class Password extends pulumi.ComponentResource { constructor( name: string, - args: Password.Args, + args: Password.Args = {}, opts: pulumi.ComponentResourceOptions = {}, ) { const optsWithDefauls = pulumi.mergeOptions(opts, { diff --git a/tests/password/infrastructure/index.ts b/tests/password/infrastructure/index.ts index b32afcb..5c8e1be 100644 --- a/tests/password/infrastructure/index.ts +++ b/tests/password/infrastructure/index.ts @@ -2,7 +2,7 @@ import { next as studion } from '@studion/infra-code-blocks'; const appName = 'password-test'; -const autoGeneratedPassword = new studion.Password(`${appName}-auto`, {}); +const autoGeneratedPassword = new studion.Password(`${appName}-auto`); const customPassword = new studion.Password(`${appName}-custom`, { value: 'customPass!', From e12f4600c505d520d511d6e0f4ff9beff83badf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Fri, 12 Dec 2025 12:11:11 +0100 Subject: [PATCH 10/14] Fix test assertion --- tests/password/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/password/index.test.ts b/tests/password/index.test.ts index cb9e2b9..8045445 100644 --- a/tests/password/index.test.ts +++ b/tests/password/index.test.ts @@ -65,7 +65,7 @@ describe('Password component deployment', () => { 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}-`; + const expectedPrefix = `${programArgs.stackName}/${programArgs.projectName}/${ctx.config.autoGeneratedPasswordName}-`; assert.ok( secretResult.Name?.startsWith(expectedPrefix), `Secret name should start with ${expectedPrefix}`, From e6cc125b00bbd6e6f3b4896a964278539b2528d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Tue, 16 Dec 2025 11:46:55 +0100 Subject: [PATCH 11/14] Wrap password value using pulumi secret --- src/v2/components/password/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/v2/components/password/index.ts b/src/v2/components/password/index.ts index 1dd5e34..4ddaab3 100644 --- a/src/v2/components/password/index.ts +++ b/src/v2/components/password/index.ts @@ -19,14 +19,11 @@ export class Password extends pulumi.ComponentResource { args: Password.Args = {}, opts: pulumi.ComponentResourceOptions = {}, ) { - const optsWithDefauls = pulumi.mergeOptions(opts, { - additionalSecretOutputs: ['value'], - }); - super('studion:Password', name, {}, optsWithDefauls); + super('studion:Password', name, {}, opts); this.name = name; if (args.value) { - this.value = pulumi.output(args.value); + this.value = pulumi.secret(args.value); } else { const password = new random.RandomPassword( `${this.name}-random-password`, From 73f3bf1e50ac652419e48b39b5e98b7db419de59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Tue, 16 Dec 2025 11:47:35 +0100 Subject: [PATCH 12/14] Implement test to check if password is a pulumi secret --- tests/password/index.test.ts | 12 ++++++++++++ tests/password/infrastructure/index.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/tests/password/index.test.ts b/tests/password/index.test.ts index 8045445..3d3f83f 100644 --- a/tests/password/index.test.ts +++ b/tests/password/index.test.ts @@ -107,4 +107,16 @@ describe('Password component deployment', () => { 'Password should match custom value', ); }); + + it('should have password value as a secret', async () => { + console.log('outputs1', await automation.getOutputs(programArgs)); + 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'); + }); }); diff --git a/tests/password/infrastructure/index.ts b/tests/password/infrastructure/index.ts index 5c8e1be..6b0aac7 100644 --- a/tests/password/infrastructure/index.ts +++ b/tests/password/infrastructure/index.ts @@ -10,5 +10,7 @@ const customPassword = new studion.Password(`${appName}-custom`, { module.exports = { autoGeneratedPassword, + autoGeneratePasswordValue: autoGeneratedPassword.value, customPassword, + customPasswordValue: customPassword.value, }; From 2cf9afe74faa43b4e841f302f877ae227e157988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Tue, 16 Dec 2025 11:56:31 +0100 Subject: [PATCH 13/14] Remove console logs --- tests/password/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/password/index.test.ts b/tests/password/index.test.ts index 3d3f83f..8ace4b6 100644 --- a/tests/password/index.test.ts +++ b/tests/password/index.test.ts @@ -109,7 +109,6 @@ describe('Password component deployment', () => { }); it('should have password value as a secret', async () => { - console.log('outputs1', await automation.getOutputs(programArgs)); const autoGeneratePasswordValue = ctx.outputs.autoGeneratePasswordValue; assert.ok( autoGeneratePasswordValue.secret, From 7297f79c15a53643efff535c0058775c9ea5dc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20=C5=A0unji=C4=87?= Date: Tue, 16 Dec 2025 13:23:02 +0100 Subject: [PATCH 14/14] Export password using ES module syntax --- tests/password/infrastructure/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/password/infrastructure/index.ts b/tests/password/infrastructure/index.ts index 6b0aac7..ebe7bce 100644 --- a/tests/password/infrastructure/index.ts +++ b/tests/password/infrastructure/index.ts @@ -3,14 +3,16 @@ 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; -module.exports = { +export { autoGeneratedPassword, - autoGeneratePasswordValue: autoGeneratedPassword.value, customPassword, - customPasswordValue: customPassword.value, + autoGeneratePasswordValue, + customPasswordValue, };