feat(bootstrap): Bun-workspace skeleton + typed config + vendored modules — T02
- Bun workspaces (packages/* + bootstrap); Pulumi nodejs runtime under
packagemanager: bun (no npm fallback needed).
- bootstrap/config.ts: typed FoundationConfig per CONTRACT_001; loadConfig()
fails closed, aggregating all missing+malformed keys in one error. Reads flat
dotted keys; image digests excluded (they live in VERSIONS, D5).
- bootstrap/Pulumi.foundation.yaml: non-secret placeholders only (RFC-5737 vm.host,
.invalid offsite); no encryptionsalt/secrets committed (D2). pulumi preview = 0
resources under the passphrase provider via gitignored file:// state backend.
- Stage-1 vendoring: packages/pulumi-{docker,vault} as @olsitec/* (source-only,
logic unchanged). vault's 5 type-only imports from modules/olsitec re-homed
verbatim into pulumi-vault/olsitec-types.ts to keep the egg self-contained.
Realizes PLAN-002 §10 T02; ADR-005 / 000_TOPOLOGY.md §5 Stage-1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
edc708b826
commit
57c4eadea7
26 changed files with 2758 additions and 0 deletions
26
packages/pulumi-vault/.editorconfig
Normal file
26
packages/pulumi-vault/.editorconfig
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
3
packages/pulumi-vault/.gitignore
vendored
Normal file
3
packages/pulumi-vault/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/bin/
|
||||
/node_modules/
|
||||
*.stack*.json
|
||||
52
packages/pulumi-vault/VENDORED.md
Normal file
52
packages/pulumi-vault/VENDORED.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# VENDORED — `@olsitec/pulumi-vault`
|
||||
|
||||
**Source (absolute path):** `/Users/andiolsi/work/olsicloud4/pulumi/modules/vault/`
|
||||
**Copy date:** 2026-06-30
|
||||
**Stage:** Stage-1 vendoring per [`documentation/000_TOPOLOGY.md` §5](../../documentation/000_TOPOLOGY.md).
|
||||
|
||||
## What this is
|
||||
|
||||
A verbatim copy of the olsicloud4 `modules/vault` Pulumi module — the Vault init/unseal
|
||||
capture (`VaultInitialization`) and the secret-engine/AppRole bootstrap
|
||||
(`VaultBootstrap`, `VaultExternalSecretsClusterAppRole`, `VaultProject`) plus the admin
|
||||
policy (`policy.ts`). Core of the foundation secret layer (ADR-004, PLAN-002 §4). At
|
||||
day-zero `bootstrap/` consumes it locally through the Bun workspace, not from a registry.
|
||||
|
||||
## What was copied
|
||||
|
||||
`index.ts`, `policy.ts`, `package.json`, `tsconfig.json`, `.editorconfig`, `.gitignore`.
|
||||
|
||||
**Not copied:** `node_modules/`, `package-lock.json` (lockfiles), `.git/`.
|
||||
|
||||
## Changes made vs. the source
|
||||
|
||||
- `package.json` `name`: `vault` → `@olsitec/pulumi-vault`; added `version` (`0.0.0`,
|
||||
pre-publish placeholder) and `main`/`types` (`index.ts`) for Bun-workspace resolution.
|
||||
- **Type-only re-home (no logic change):** the upstream `index.ts` imports five
|
||||
*purely type-level* declarations from its sibling module `../../modules/olsitec`
|
||||
(`OlsitecProjectFeatureFlags`, `OlsitecCredentialTypes`, `GitProjectCredentials`,
|
||||
`OciRegistryCredentials`, `MinioBackupProjectCredentials`). That sibling transitively
|
||||
pulls in `modules/minio`, `modules/gitlab`, and `modules/kubernetes`, none of which
|
||||
belong in the foundation egg and none of which are vendored. To keep this package
|
||||
self-contained, those five type declarations were copied **verbatim** into a new local
|
||||
file `olsitec-types.ts`, and the one import line in `index.ts` was re-pointed from
|
||||
`../../modules/olsitec` to `./olsitec-types`. This is the **only** edit to `index.ts`;
|
||||
no runtime/behavioural logic changed.
|
||||
- `tsconfig.json` `files`: added `policy.ts` and `olsitec-types.ts` so the package
|
||||
type-checks standalone (`tsc --noEmit`).
|
||||
|
||||
> **Note (out of scope for T02):** `VaultProject` and `VaultBootstrap` still reference
|
||||
> minio/garage/cockroach/mongo credential shapes inherited from the Layer-1 olsitec module.
|
||||
> The foundation egg only needs `VaultInitialization` (init/unseal capture) + `VaultBootstrap`.
|
||||
> Trimming the unused Layer-1 surface is a deliberate later refactor (000_TOPOLOGY.md §5.1
|
||||
> "refactor for Layer 0"), NOT part of Stage-1 vendoring — Stage 1 preserves the source as-is.
|
||||
|
||||
## Lifecycle (000_TOPOLOGY.md §5)
|
||||
|
||||
- **Stage 1 — VENDOR (this commit):** copied here; consumed locally via Bun workspace.
|
||||
- **Stage 2 — PUBLISH (later task):** CI publishes `@olsitec/pulumi-vault@<semver>` to the
|
||||
foundation Forgejo npm registry once it exists.
|
||||
- **Stage 3 — CONSUME (steady state):** downstream switches imports to the published package;
|
||||
the old module is frozen then removed.
|
||||
|
||||
Do not refactor the vendored logic here beyond the type-only re-home documented above.
|
||||
770
packages/pulumi-vault/index.ts
Normal file
770
packages/pulumi-vault/index.ts
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
// modules/vault/index.ts
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
import * as vault from "@pulumi/vault";
|
||||
import { RandomPassword } from "@pulumi/random";
|
||||
|
||||
import { adminPolicyContent } from "./policy";
|
||||
import { Secret } from "@pulumi/vault/generic";
|
||||
|
||||
import {
|
||||
type OlsitecProjectFeatureFlags,
|
||||
type OlsitecCredentialTypes,
|
||||
type GitProjectCredentials,
|
||||
type OciRegistryCredentials,
|
||||
type MinioBackupProjectCredentials,
|
||||
} from "./olsitec-types";
|
||||
|
||||
interface VaultInitializationArgs {
|
||||
url: pulumi.Input<string>;
|
||||
shares: number;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
interface InitResponse {
|
||||
keys: string[];
|
||||
root_token: string;
|
||||
}
|
||||
|
||||
export class VaultInitialization extends pulumi.ComponentResource {
|
||||
public readonly unsealKeys: pulumi.Output<string[] | undefined>;
|
||||
public readonly rootToken: pulumi.Output<string>;
|
||||
public readonly url: pulumi.Output<string>;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
args: VaultInitializationArgs,
|
||||
opts?: pulumi.ComponentResourceOptions,
|
||||
) {
|
||||
super("custom:resource:VaultInitialization", name, {}, opts);
|
||||
|
||||
const config = new pulumi.Config("vaultCredentials"); // Specify a namespace if needed
|
||||
const storedUnsealKeys = config.getSecret("unsealKeys");
|
||||
const storedRootToken = config.getSecret("rootToken");
|
||||
|
||||
this.url = pulumi.output(args.url);
|
||||
|
||||
const initResult = pulumi
|
||||
.all([
|
||||
args.url,
|
||||
pulumi.output(args.shares),
|
||||
pulumi.output(args.threshold),
|
||||
storedUnsealKeys,
|
||||
storedRootToken,
|
||||
])
|
||||
.apply(async ([url, shares, threshold, keys, token]) => {
|
||||
// Check if it's a preview; skip initialization if so
|
||||
if (pulumi.runtime.isDryRun()) {
|
||||
pulumi.log.info("Skipping Vault initialization during preview.");
|
||||
return { keys: [], root_token: "" };
|
||||
}
|
||||
|
||||
// Implement polling to ensure Vault is ready
|
||||
await VaultInitialization.waitForVault(url);
|
||||
|
||||
// Perform the initialization
|
||||
pulumi.log.info(`Attempting to initialize Vault at ${url}/v1/sys/init`);
|
||||
const response = await fetch(`${url}/v1/sys/init`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Referer: url,
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret_shares: shares,
|
||||
secret_threshold: threshold,
|
||||
}),
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMsg = errorData.errors
|
||||
? errorData.errors.join(", ")
|
||||
: response.statusText;
|
||||
|
||||
if (
|
||||
response.status === 400 &&
|
||||
errorMsg.toLowerCase().includes("already initialized")
|
||||
) {
|
||||
if (keys && token) {
|
||||
pulumi.log.info(
|
||||
"Vault is already initialized. Using stored keys and token.",
|
||||
);
|
||||
await this.unsealVault(url, JSON.parse(keys));
|
||||
return {
|
||||
keys: JSON.parse(keys), // Assuming keys are stored as a JSON string
|
||||
root_token: token,
|
||||
};
|
||||
}
|
||||
pulumi.log.info("Vault is already initialized.");
|
||||
return { keys: [], root_token: "" };
|
||||
}
|
||||
|
||||
throw new Error(`Failed to initialize Vault: ${errorMsg}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as InitResponse;
|
||||
pulumi.log.info("Vault initialized successfully.");
|
||||
await this.unsealVault(url, result.keys);
|
||||
return {
|
||||
keys: result.keys,
|
||||
root_token: result.root_token,
|
||||
};
|
||||
});
|
||||
|
||||
// Assign the outputs
|
||||
this.unsealKeys = initResult.apply((result) =>
|
||||
pulumi.secret((result.keys as string[]) ?? []),
|
||||
);
|
||||
this.rootToken = initResult.apply((result) =>
|
||||
pulumi.secret(result.root_token),
|
||||
);
|
||||
|
||||
// Register outputs
|
||||
this.registerOutputs({
|
||||
unsealKeys: this.unsealKeys,
|
||||
rootToken: this.rootToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the Vault service is reachable by polling the /v1/sys/init endpoint.
|
||||
* Retries up to 10 times with a 2-second delay between attempts.
|
||||
*/
|
||||
private static async waitForVault(url: string): Promise<void> {
|
||||
const maxRetries = 12;
|
||||
const retryDelay = 10000; // 10 seconds
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await fetch(`${url}/v1/sys/init`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok || response.status === 403) {
|
||||
// 403 means Vault is already initialized
|
||||
pulumi.log.info("Vault is reachable and ready.");
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
pulumi.log.info(
|
||||
`Attempt ${attempt}: Failed to reach Vault - ${error.message}`,
|
||||
);
|
||||
// Ignore errors and retry
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
pulumi.log.info(
|
||||
`Vault not ready, retrying in ${retryDelay / 1000} seconds...`,
|
||||
);
|
||||
await new Promise((res) => setTimeout(res, retryDelay));
|
||||
} else {
|
||||
throw new Error(
|
||||
"Vault service is not reachable after multiple attempts.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Unseals the Vault service using the provided unseal keys.
|
||||
*/
|
||||
public async unsealVault(url: string, unsealKeys: string[]): Promise<void> {
|
||||
if (unsealKeys.length === 0) {
|
||||
throw new Error(
|
||||
"Failed to unseal Vault: insufficient unseal keys provided.",
|
||||
);
|
||||
}
|
||||
const sealStatusResponse = await fetch(`${url}/v1/sys/seal-status`, {
|
||||
method: "GET",
|
||||
});
|
||||
const sealStatus = await sealStatusResponse.json();
|
||||
if (!sealStatus.sealed) {
|
||||
pulumi.log.info("Vault is already unsealed.");
|
||||
return;
|
||||
}
|
||||
for (const key of unsealKeys) {
|
||||
const unsealResponse = await fetch(`${url}/v1/sys/unseal`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key }),
|
||||
method: "PUT",
|
||||
});
|
||||
if (!unsealResponse.ok) {
|
||||
throw new Error(`Failed to unseal Vault: ${unsealResponse.statusText}`);
|
||||
}
|
||||
const unsealResult = await unsealResponse.json();
|
||||
if (!unsealResult.sealed) {
|
||||
pulumi.log.info("Vault unsealed successfully.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to unseal Vault.");
|
||||
}
|
||||
}
|
||||
|
||||
interface VaultBootstrapArgs {
|
||||
prefix: string;
|
||||
url: pulumi.Input<string>;
|
||||
token: pulumi.Input<string>;
|
||||
userNames: string[];
|
||||
secrets?: {
|
||||
name: string;
|
||||
path?: string;
|
||||
data: pulumi.Input<{ [key: string]: any }>;
|
||||
disableRead?: boolean;
|
||||
}[];
|
||||
}
|
||||
export class VaultBootstrap extends pulumi.ComponentResource {
|
||||
public readonly url: pulumi.Output<string>;
|
||||
public readonly vaultMount: vault.Mount;
|
||||
public readonly vaultProvider: vault.Provider;
|
||||
public readonly sourceCredentialsPath: pulumi.Output<string>;
|
||||
public readonly path: pulumi.Output<string>;
|
||||
public readonly roleId: pulumi.Output<string>;
|
||||
public readonly secretId: pulumi.Output<string>;
|
||||
public readonly users: {
|
||||
username: string;
|
||||
password: pulumi.Output<string>;
|
||||
}[];
|
||||
private readonly secrets: Secret[];
|
||||
constructor(
|
||||
name: string,
|
||||
args: VaultBootstrapArgs,
|
||||
opts?: pulumi.ComponentResourceOptions,
|
||||
) {
|
||||
super("custom:resource:VaultBootstrap", name, {}, opts);
|
||||
this.users = [];
|
||||
this.url = pulumi.output(args.url);
|
||||
// pulumi.output(args.token).apply((token) => {
|
||||
// console.log("VaultBootstrap: token: ", token);
|
||||
// pulumi.log.info(`VaultBootstrap: token: ${token}`);
|
||||
// });
|
||||
this.vaultProvider = new vault.Provider(
|
||||
`${args.prefix}-vault-provider`,
|
||||
{
|
||||
address: args.url,
|
||||
token: args.token,
|
||||
},
|
||||
{ parent: this },
|
||||
);
|
||||
|
||||
this.vaultMount = new vault.Mount(
|
||||
`${args.prefix}-vault-mount`,
|
||||
{
|
||||
path: args.prefix,
|
||||
type: "kv",
|
||||
options: {
|
||||
version: "2",
|
||||
},
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this },
|
||||
);
|
||||
this.sourceCredentialsPath = this.vaultMount.path;
|
||||
|
||||
const sourceCredentialsSecret = new vault.generic.Secret(
|
||||
`${args.prefix}-vault-source-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/source-credentials`,
|
||||
dataJson: "{}",
|
||||
disableRead: true,
|
||||
},
|
||||
{
|
||||
provider: this.vaultProvider,
|
||||
parent: this,
|
||||
dependsOn: this.vaultMount,
|
||||
},
|
||||
);
|
||||
|
||||
this.secrets =
|
||||
args.secrets?.map((secret) => {
|
||||
return new vault.generic.Secret(
|
||||
`${args.prefix}-vault-secret-${secret.name}`,
|
||||
{
|
||||
path: secret.path ? secret.path : `${args.prefix}/${secret.name}`,
|
||||
dataJson: pulumi
|
||||
.output(secret.data)
|
||||
.apply((data) => JSON.stringify(data)),
|
||||
disableRead: secret.disableRead,
|
||||
},
|
||||
{
|
||||
provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
}) || [];
|
||||
|
||||
const pulumiPolicy = new vault.Policy(
|
||||
`${args.prefix}-vault-policy-pulumi`,
|
||||
{
|
||||
name: `pulumi-${args.prefix}-vault-policy-pulumi`,
|
||||
policy: adminPolicyContent,
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this },
|
||||
);
|
||||
|
||||
const pulumiAuthBackend = new vault.AuthBackend(
|
||||
`${args.prefix}-vault-auth-backend-pulumi`,
|
||||
{
|
||||
type: "approle",
|
||||
path: `pulumi-${args.prefix}-vault-auth-backend-pulumi`,
|
||||
},
|
||||
{
|
||||
provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const pulumiAuthBackendRole = new vault.approle.AuthBackendRole(
|
||||
`${args.prefix}-vault-auth-backend-role-pulumi`,
|
||||
{
|
||||
backend: pulumiAuthBackend.path,
|
||||
roleName: `pulumi-${args.prefix}-vault-auth-backend-role-pulumi`,
|
||||
tokenPolicies: [pulumiPolicy.name],
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this },
|
||||
);
|
||||
|
||||
const pulumiSecretId = new vault.approle.AuthBackendRoleSecretID(
|
||||
`${args.prefix}-vault-auth-backend-role-secret-id-pulumi`,
|
||||
{
|
||||
backend: pulumiAuthBackend.path,
|
||||
roleName: pulumiAuthBackendRole.roleName,
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this },
|
||||
);
|
||||
|
||||
const userpassAuth = new vault.AuthBackend(
|
||||
`${args.prefix}-vault-auth-backend-userpass`,
|
||||
{
|
||||
type: "userpass",
|
||||
path: `userpass`,
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this },
|
||||
);
|
||||
|
||||
// Create the admin policy
|
||||
const adminPolicy = new vault.Policy(
|
||||
`${args.prefix}-vault-policy-admin`,
|
||||
{
|
||||
name: `pulumi-${args.prefix}-vault-policy-admin`,
|
||||
policy: adminPolicyContent,
|
||||
},
|
||||
{ provider: this.vaultProvider, parent: this, dependsOn: userpassAuth },
|
||||
);
|
||||
// Create the admin user
|
||||
this.users = args.userNames.map((userName) => {
|
||||
const password = new RandomPassword(`${userName}-password`, {
|
||||
length: 22,
|
||||
special: false,
|
||||
}).result;
|
||||
const endpoint = new vault.generic.Endpoint(
|
||||
"admin-user",
|
||||
{
|
||||
path: pulumi.interpolate`auth/${userpassAuth.path}/users/${userName}`,
|
||||
dataJson: pulumi
|
||||
.all({
|
||||
policy: adminPolicy.name,
|
||||
password: password,
|
||||
})
|
||||
.apply((data) => {
|
||||
return JSON.stringify({
|
||||
policies: [data.policy],
|
||||
password: data.password,
|
||||
});
|
||||
}),
|
||||
},
|
||||
{
|
||||
provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
return { username: userName, password };
|
||||
});
|
||||
|
||||
this.path = pulumiAuthBackend.path;
|
||||
this.roleId = pulumiAuthBackendRole.roleId;
|
||||
this.secretId = pulumiSecretId.secretId;
|
||||
|
||||
this.registerOutputs({
|
||||
path: this.path,
|
||||
roleId: this.roleId,
|
||||
secretId: this.secretId,
|
||||
users: this.users.map((user) => ({
|
||||
username: user.username,
|
||||
password: pulumi.secret(user.password),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface VaultExternalSecretsClusterAppRoleArgs {
|
||||
// url: pulumi.Input<string>;
|
||||
// token: pulumi.Input<string>;
|
||||
prefix: string;
|
||||
vaultMountId: pulumi.Input<string>;
|
||||
}
|
||||
|
||||
export class VaultExternalSecretsClusterAppRole extends pulumi.ComponentResource {
|
||||
public readonly path: pulumi.Output<string>;
|
||||
public readonly roleId: pulumi.Output<string>;
|
||||
public readonly secretId: pulumi.Output<string>;
|
||||
// public readonly vaultProvider: vault.Provider;
|
||||
public readonly vaultMount: vault.Mount;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
args: VaultExternalSecretsClusterAppRoleArgs,
|
||||
opts?: pulumi.ResourceOptions,
|
||||
) {
|
||||
super("custom:resource:VaultExternalSecretsClusterAppRole", name, {}, opts);
|
||||
// this.vaultProvider = new vault.Provider(
|
||||
// `${args.prefix}-vault-provider`,
|
||||
// {
|
||||
// address: args.url,
|
||||
// token: args.token,
|
||||
// },
|
||||
// { parent: this },
|
||||
// );
|
||||
this.vaultMount = vault.Mount.get(
|
||||
`${args.prefix}-vault-mount`,
|
||||
args.vaultMountId,
|
||||
{
|
||||
path: args.prefix,
|
||||
type: "kv",
|
||||
options: {
|
||||
version: "2",
|
||||
},
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
// Define the policy
|
||||
const paths = {
|
||||
[`${args.prefix}/data`]: '["read", "list"]',
|
||||
[`${args.prefix}/data/*`]: '["read", "list"]',
|
||||
[`${args.prefix}/metadata`]: '["read", "list"]',
|
||||
[`${args.prefix}/metadata/*`]: '["read", "list"]',
|
||||
};
|
||||
|
||||
let policyStatements = "";
|
||||
for (const [path, capabilities] of Object.entries(paths)) {
|
||||
policyStatements += `
|
||||
path "${path}" {
|
||||
capabilities = ${capabilities}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const externalSecretsPolicy = new vault.Policy(
|
||||
`${args.prefix}-vault-policy-external-secrets`,
|
||||
{
|
||||
name: `pulumi-${args.prefix}-vault-policy-external-secrets`,
|
||||
policy: policyStatements,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsAuthBackend = new vault.AuthBackend(
|
||||
`${args.prefix}-vault-auth-backend-external-secrets`,
|
||||
{
|
||||
type: "approle",
|
||||
path: `pulumi-${args.prefix}-vault-auth-backend-external-secrets`,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider,
|
||||
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsAuthBackendRole = new vault.approle.AuthBackendRole(
|
||||
`${args.prefix}-vault-auth-backend-role-external-secrets`,
|
||||
{
|
||||
backend: externalSecretsAuthBackend.path,
|
||||
roleName: `pulumi-${args.prefix}-vault-auth-backend-role-external-secrets`,
|
||||
tokenPolicies: [externalSecretsPolicy.name],
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsSecretId = new vault.approle.AuthBackendRoleSecretID(
|
||||
`${args.prefix}-vault-auth-backend-role-secret-id-external-secrets`,
|
||||
{
|
||||
backend: externalSecretsAuthBackend.path,
|
||||
roleName: externalSecretsAuthBackendRole.roleName,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider,
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
this.path = externalSecretsAuthBackend.path;
|
||||
this.roleId = externalSecretsAuthBackendRole.roleId;
|
||||
this.secretId = externalSecretsSecretId.secretId;
|
||||
|
||||
this.registerOutputs({
|
||||
path: this.path,
|
||||
roleId: this.roleId,
|
||||
secretId: this.secretId,
|
||||
vaultMount: this.vaultMount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface VaultProjectArgs {
|
||||
// vaultProvider: vault.Provider;
|
||||
// vaultMount: vault.Mount;
|
||||
prefix: string;
|
||||
projectName: string;
|
||||
projectStage: string;
|
||||
featureFlags: OlsitecProjectFeatureFlags[];
|
||||
credentials: OlsitecCredentialTypes;
|
||||
gitCredentials: GitProjectCredentials;
|
||||
ociRegistryCredentials: OciRegistryCredentials;
|
||||
minioBackupProjectCredentials?: MinioBackupProjectCredentials;
|
||||
additionalPolicyPaths?: string[];
|
||||
}
|
||||
|
||||
export class VaultProject extends pulumi.ComponentResource {
|
||||
// public readonly vaultProvider: vault.Provider;
|
||||
public readonly path: pulumi.Output<string>;
|
||||
public readonly roleId: pulumi.Output<string>;
|
||||
public readonly secretId: pulumi.Output<string>;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
args: VaultProjectArgs,
|
||||
opts?: pulumi.ResourceOptions,
|
||||
) {
|
||||
super("custom:resource:VaultProject", name, {}, opts);
|
||||
// this.vaultProvider = args.vaultProvider;
|
||||
|
||||
// Define the policy
|
||||
let paths = {
|
||||
[`${args.prefix}/data/${args.projectName}/${args.projectStage}`]:
|
||||
'["read", "list"]',
|
||||
[`${args.prefix}/data/${args.projectName}/${args.projectStage}/*`]:
|
||||
'["read", "list"]',
|
||||
[`${args.prefix}/metadata/${args.projectName}/${args.projectStage}`]:
|
||||
'["read", "list"]',
|
||||
[`${args.prefix}/metadata/${args.projectName}/${args.projectStage}/*`]:
|
||||
'["read", "list"]',
|
||||
};
|
||||
|
||||
for (const path of args.additionalPolicyPaths ?? []) {
|
||||
paths = {
|
||||
...paths,
|
||||
[`${args.prefix}/data/${path}`]: '["read", "list"]',
|
||||
[`${args.prefix}/data/${path}/*`]: '["read", "list"]',
|
||||
[`${args.prefix}/metadata/${path}`]: '["read", "list"]',
|
||||
[`${args.prefix}/metadata/${path}/*`]: '["read", "list"]',
|
||||
};
|
||||
}
|
||||
let policyStatements = "";
|
||||
for (const [path, capabilities] of Object.entries(paths)) {
|
||||
policyStatements += `
|
||||
path "${path}" {
|
||||
capabilities = ${capabilities}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const externalSecretsPolicy = new vault.Policy(
|
||||
`${name}-vault-policy-external-secrets`,
|
||||
{
|
||||
name: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-policy-external-secrets`,
|
||||
policy: policyStatements,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsAuthBackend = new vault.AuthBackend(
|
||||
`${name}-vault-auth-backend-external-secrets`,
|
||||
{
|
||||
type: "approle",
|
||||
path: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-auth-backend-external-secrets`,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsAuthBackendRole = new vault.approle.AuthBackendRole(
|
||||
`${name}-vault-auth-backend-role`,
|
||||
{
|
||||
backend: externalSecretsAuthBackend.path,
|
||||
roleName: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-auth-backend-role`,
|
||||
tokenPolicies: [externalSecretsPolicy.name],
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
const externalSecretsAuthBackendRoleSecretId =
|
||||
new vault.approle.AuthBackendRoleSecretID(
|
||||
`${name}-vault-auth-backend-role-secret-id-external-secrets`,
|
||||
{
|
||||
backend: externalSecretsAuthBackend.path,
|
||||
roleName: externalSecretsAuthBackendRole.roleName,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
// minioBackup
|
||||
if (
|
||||
args.featureFlags.includes("minioBackup") &&
|
||||
args.minioBackupProjectCredentials
|
||||
) {
|
||||
const minioBackupCredentialsSecret = new vault.generic.Secret(
|
||||
`${name}-secret-backup-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/${args.projectName}/${args.projectStage}/backup-credentials`,
|
||||
dataJson: pulumi
|
||||
.all({
|
||||
minioBackupAccessKey:
|
||||
args.minioBackupProjectCredentials.minioBackupAccessKey,
|
||||
minioBackupSecretKey:
|
||||
args.minioBackupProjectCredentials.minioBackupSecretKey,
|
||||
minioBackupEndpoint:
|
||||
args.minioBackupProjectCredentials.minioBackupEndpoint,
|
||||
})
|
||||
.apply((data) => JSON.stringify(data)),
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// project-credentials
|
||||
const projectCredentialsSecret = new vault.generic.Secret(
|
||||
`${name}-secret-project-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/${args.projectName}/${args.projectStage}/project-credentials`,
|
||||
dataJson: "{}",
|
||||
disableRead: true,
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
// service-credentials
|
||||
const serviceCredentialsSecret = new vault.generic.Secret(
|
||||
`${name}-secret-service-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/${args.projectName}/${args.projectStage}/service-credentials`,
|
||||
dataJson: pulumi
|
||||
.all({
|
||||
cockroachdbAdminUser: args.credentials.cockroachdbAdminUser,
|
||||
cockroachdbAdminPassword: args.credentials.cockroachdbAdminPassword,
|
||||
cockroachdbServiceUser: args.credentials.cockroachdbServiceUser,
|
||||
cockroachdbServicePassword:
|
||||
args.credentials.cockroachdbServicePassword,
|
||||
mongodbAdminUser: args.credentials.mongodbAdminUser,
|
||||
mongodbAdminPassword: args.credentials.mongodbAdminPassword,
|
||||
mongodbBackupUser: args.credentials.mongodbBackupUser,
|
||||
mongodbBackupPassword: args.credentials.mongodbBackupPassword,
|
||||
mongodbServiceUser: args.credentials.mongodbServiceUser,
|
||||
mongodbServicePassword: args.credentials.mongodbServicePassword,
|
||||
mongodbKeyfile: args.credentials.mongodbKeyfile,
|
||||
minioAdminUser: args.credentials.minioAdminUser,
|
||||
minioAdminPassword: args.credentials.minioAdminPassword,
|
||||
minioServiceUser: args.credentials.minioServiceUser,
|
||||
minioServicePassword: args.credentials.minioServicePassword,
|
||||
rustfsAdminUser: args.credentials.rustfsAdminUser,
|
||||
rustfsAdminPassword: args.credentials.rustfsAdminPassword,
|
||||
rustfsServiceUser: args.credentials.rustfsServiceUser,
|
||||
rustfsServicePassword: args.credentials.rustfsServicePassword,
|
||||
garageRpcSecret: args.credentials.garageRpcSecret,
|
||||
garageAdminToken: args.credentials.garageAdminToken,
|
||||
garageServiceKeyId: args.credentials.garageServiceKeyId,
|
||||
garageServiceKeySecret: args.credentials.garageServiceKeySecret,
|
||||
natsToken: args.credentials.natsToken,
|
||||
grafanaAdminPassword: args.credentials.grafanaAdminPassword,
|
||||
postgresUser: args.credentials.postgresUser,
|
||||
postgresPassword: args.credentials.postgresPassword,
|
||||
postgresServiceUser: args.credentials.postgresServiceUser,
|
||||
postgresServicePassword: args.credentials.postgresServicePassword,
|
||||
basicAuthUser: args.credentials.basicAuthUser,
|
||||
basicAuthPassword: args.credentials.basicAuthPassword,
|
||||
basicAuthHtpasswd: args.credentials.basicAuthHtpasswd,
|
||||
nominatimPassword: args.credentials.nominatimPassword,
|
||||
})
|
||||
.apply((data) => JSON.stringify(data)),
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
// git-credentials
|
||||
const gitCredentialsSecret = new vault.generic.Secret(
|
||||
`${name}-secret-git-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/${args.projectName}/${args.projectStage}/git-credentials`,
|
||||
dataJson: pulumi
|
||||
.all({
|
||||
gitArgocdUser: args.gitCredentials.gitArgocdUser,
|
||||
gitArgocdPassword: args.gitCredentials.gitArgocdToken,
|
||||
})
|
||||
.apply((data) => JSON.stringify(data)),
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
// registry-credentials
|
||||
const ociRegistryCredentialsSecret = new vault.generic.Secret(
|
||||
`${name}-secret-oci-registry-credentials`,
|
||||
{
|
||||
path: `${args.prefix}/${args.projectName}/${args.projectStage}/registry-credentials`,
|
||||
dataJson: pulumi
|
||||
.all({
|
||||
ociRegistryAddress: args.ociRegistryCredentials.ociRegistryAddress,
|
||||
ociRegistryUser: args.ociRegistryCredentials.ociRegistryUser,
|
||||
ociRegistryPassword:
|
||||
args.ociRegistryCredentials.ociRegistryPassword,
|
||||
})
|
||||
.apply((data) => JSON.stringify(data)),
|
||||
},
|
||||
{
|
||||
// provider: this.vaultProvider
|
||||
parent: this,
|
||||
},
|
||||
);
|
||||
|
||||
this.path = externalSecretsAuthBackend.path;
|
||||
this.roleId = externalSecretsAuthBackendRole.roleId;
|
||||
this.secretId = externalSecretsAuthBackendRoleSecretId.secretId;
|
||||
|
||||
this.registerOutputs({
|
||||
path: this.path,
|
||||
roleId: this.roleId,
|
||||
secretId: this.secretId,
|
||||
});
|
||||
}
|
||||
}
|
||||
90
packages/pulumi-vault/olsitec-types.ts
Normal file
90
packages/pulumi-vault/olsitec-types.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// olsitec-types.ts
|
||||
//
|
||||
// VENDORING NOTE (Stage-1, 2026-06-30 — see VENDORED.md):
|
||||
// The upstream olsicloud4 `modules/vault/index.ts` imports five PURELY TYPE-LEVEL
|
||||
// declarations from its sibling `../../modules/olsitec`:
|
||||
//
|
||||
// OlsitecProjectFeatureFlags, OlsitecCredentialTypes,
|
||||
// GitProjectCredentials, OciRegistryCredentials, MinioBackupProjectCredentials
|
||||
//
|
||||
// That sibling module transitively pulls in `modules/minio`, `modules/gitlab`,
|
||||
// and `modules/kubernetes`, none of which belong in the foundation egg and none
|
||||
// of which are vendored. To keep the vault module SELF-CONTAINED inside the
|
||||
// foundation workspace WITHOUT changing any runtime behaviour, these five type
|
||||
// declarations are copied here VERBATIM from
|
||||
// `olsicloud4/pulumi/modules/olsitec/index.ts` (definitions only — no logic, no
|
||||
// ComponentResource), and `index.ts`'s import is re-pointed from
|
||||
// `../../modules/olsitec` to `./olsitec-types`.
|
||||
//
|
||||
// This is a type-only re-home; the vault module's logic is unchanged.
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
|
||||
export type OlsitecCredentialTypes = {
|
||||
minioBackupEndpoint?: string;
|
||||
minioBackupAccessKey?: pulumi.Output<string>;
|
||||
minioBackupSecretKey?: pulumi.Output<string>;
|
||||
cockroachdbAdminUser?: string;
|
||||
cockroachdbAdminPassword?: pulumi.Output<string>;
|
||||
cockroachdbServiceUser?: string;
|
||||
cockroachdbServicePassword?: pulumi.Output<string>;
|
||||
mongodbAdminUser?: string;
|
||||
mongodbAdminPassword?: pulumi.Output<string>;
|
||||
mongodbBackupUser?: string;
|
||||
mongodbBackupPassword?: pulumi.Output<string>;
|
||||
mongodbServiceUser?: string;
|
||||
mongodbServicePassword?: pulumi.Output<string>;
|
||||
mongodbKeyfile?: pulumi.Output<string>;
|
||||
minioAdminUser?: string;
|
||||
minioAdminPassword?: pulumi.Output<string>;
|
||||
minioServiceUser?: string;
|
||||
minioServicePassword?: pulumi.Output<string>;
|
||||
rustfsAdminUser?: string;
|
||||
rustfsAdminPassword?: pulumi.Output<string>;
|
||||
rustfsServiceUser?: string;
|
||||
rustfsServicePassword?: pulumi.Output<string>;
|
||||
garageRpcSecret?: pulumi.Output<string>;
|
||||
garageAdminToken?: pulumi.Output<string>;
|
||||
garageServiceKeyId?: pulumi.Output<string>;
|
||||
garageServiceKeySecret?: pulumi.Output<string>;
|
||||
natsToken?: pulumi.Output<string>;
|
||||
grafanaAdminPassword?: pulumi.Output<string>;
|
||||
postgresUser?: string;
|
||||
postgresPassword?: pulumi.Output<string>;
|
||||
postgresServiceUser?: string;
|
||||
postgresServicePassword?: pulumi.Output<string>;
|
||||
basicAuthUser?: string;
|
||||
basicAuthPassword?: pulumi.Output<string>;
|
||||
basicAuthHtpasswd?: pulumi.Output<string>;
|
||||
nominatimPassword?: pulumi.Output<string>;
|
||||
};
|
||||
|
||||
export type OlsitecProjectFeatureFlags =
|
||||
| "minioBackup"
|
||||
| "cockroachdb"
|
||||
| "vault"
|
||||
| "mongodb"
|
||||
| "nats"
|
||||
| "minio"
|
||||
| "rustfs"
|
||||
| "garage"
|
||||
| "grafana"
|
||||
| "postgres"
|
||||
| "basicAuth"
|
||||
| "nominatim";
|
||||
|
||||
export type OciRegistryCredentials = {
|
||||
ociRegistryAddress: string;
|
||||
ociRegistryUser: pulumi.Input<string>;
|
||||
ociRegistryPassword: pulumi.Input<string>;
|
||||
};
|
||||
|
||||
export type GitProjectCredentials = {
|
||||
gitArgocdUser: pulumi.Input<string>;
|
||||
gitArgocdToken: pulumi.Input<string>;
|
||||
};
|
||||
|
||||
export type MinioBackupProjectCredentials = {
|
||||
minioBackupEndpoint: pulumi.Input<string>;
|
||||
minioBackupAccessKey: pulumi.Output<string>;
|
||||
minioBackupSecretKey: pulumi.Output<string>;
|
||||
};
|
||||
19
packages/pulumi-vault/package.json
Normal file
19
packages/pulumi-vault/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@olsitec/pulumi-vault",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"types": "index.ts",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@pulumi/eslint-plugin": "^0.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pulumi/pulumi": "^3.138.0",
|
||||
"@pulumi/random": "^4.16.8",
|
||||
"@pulumi/vault": "^4.5.8"
|
||||
}
|
||||
}
|
||||
72
packages/pulumi-vault/policy.ts
Normal file
72
packages/pulumi-vault/policy.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
export const adminPolicyContent = `
|
||||
# Manage auth methods broadly across Vault
|
||||
path "auth/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# Create, update, and delete auth methods
|
||||
path "sys/auth/*" {
|
||||
capabilities = ["create", "update", "delete", "sudo"]
|
||||
}
|
||||
|
||||
# List auth methods
|
||||
path "sys/auth" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# Create and manage ACL policies
|
||||
path "sys/policies/acl/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# List ACL policies
|
||||
path "sys/policies/acl" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
|
||||
# Create and manage secrets engines broadly across Vault.
|
||||
path "sys/mounts/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# List enabled secrets engines
|
||||
path "sys/mounts" {
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
|
||||
# List, create, update, and delete key/value secrets at secret/
|
||||
path "secret/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# Manage transit secrets engine
|
||||
path "transit/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
||||
}
|
||||
|
||||
# Read health checks
|
||||
path "sys/health" {
|
||||
capabilities = ["read", "sudo"]
|
||||
}
|
||||
|
||||
path "*/metadata" {
|
||||
capabilities = ["create", "update", "read", "delete", "list"]
|
||||
}
|
||||
|
||||
path "*/metadata/*" {
|
||||
capabilities = ["create", "update", "read", "delete", "list"]
|
||||
}
|
||||
|
||||
path "*/data" {
|
||||
capabilities = ["create", "update", "read", "delete", "list"]
|
||||
}
|
||||
|
||||
path "*/data/*" {
|
||||
capabilities = ["create", "update", "read", "delete", "list"]
|
||||
}
|
||||
|
||||
path "*" {
|
||||
capabilities = ["create", "update", "read", "delete", "list"]
|
||||
}
|
||||
|
||||
`;
|
||||
20
packages/pulumi-vault/tsconfig.json
Normal file
20
packages/pulumi-vault/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "bin",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"pretty": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"policy.ts",
|
||||
"olsitec-types.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue