diff --git a/.github/scripts/src/pulumi/preview.ts b/.github/scripts/src/pulumi/preview.ts index 91abebe..0b4297f 100644 --- a/.github/scripts/src/pulumi/preview.ts +++ b/.github/scripts/src/pulumi/preview.ts @@ -24,7 +24,7 @@ const { prId } = await yargs(hideBin(process.argv)) export async function main() { const envClient = EnvClient.create(EnvClientStrategy.SOPS); const { - azurePulumiLocation, + pulumiBackendUrl, githubAppAppId, githubAppInstallationId, githubAppPrivateKey, @@ -46,7 +46,7 @@ export async function main() { stackName: "main", workDir: ".", envs: { - PULUMI_BACKEND_URL: azurePulumiLocation, + PULUMI_BACKEND_URL: pulumiBackendUrl, ...env, }, }); @@ -92,10 +92,10 @@ ${res.stdout} } function parseCiEnv(ciEnv: Record) { - const azurePulumiLocation = (() => { - const v = ciEnv["AZURE_PULUMI_LOCATION"]; + const pulumiBackendUrl = (() => { + const v = ciEnv["PULUMI_BACKEND_URL"]; if (!v) { - throw new Error("Missing AZURE_PULUMI_LOCATION from .env.ci"); + throw new Error("Missing PULUMI_BACKEND_URL from .env.ci"); } return v; })(); @@ -125,7 +125,7 @@ function parseCiEnv(ciEnv: Record) { })(); return { - azurePulumiLocation, + pulumiBackendUrl, githubAppAppId, githubAppInstallationId, githubAppPrivateKey, diff --git a/.github/scripts/src/pulumi/refresh.ts b/.github/scripts/src/pulumi/refresh.ts index 5869491..0227b20 100644 --- a/.github/scripts/src/pulumi/refresh.ts +++ b/.github/scripts/src/pulumi/refresh.ts @@ -8,7 +8,7 @@ import _ from "lodash"; export async function main() { const envClient = EnvClient.create(EnvClientStrategy.SOPS); - const { azurePulumiLocation, env } = parseCiEnv( + const { pulumiBackendUrl, env } = parseCiEnv( _.merge( await envClient.readFromEnv("secrets.yaml"), await envClient.readFromEnv("secrets.administrator.yaml"), @@ -20,7 +20,7 @@ export async function main() { stackName: "main", workDir: ".", envs: { - PULUMI_BACKEND_URL: azurePulumiLocation, + PULUMI_BACKEND_URL: pulumiBackendUrl, ...env, }, }); @@ -42,16 +42,16 @@ export async function main() { } function parseCiEnv(ciEnv: Record) { - const azurePulumiLocation = (() => { - const v = ciEnv["AZURE_PULUMI_LOCATION"]; + const pulumiBackendUrl = (() => { + const v = ciEnv["PULUMI_BACKEND_URL"]; if (!v) { - throw new Error("Missing AZURE_PULUMI_LOCATION from .env.ci"); + throw new Error("Missing PULUMI_BACKEND_URL from .env.ci"); } return v; })(); return { - azurePulumiLocation, + pulumiBackendUrl, env: ciEnv, }; } diff --git a/.github/scripts/src/pulumi/up.ts b/.github/scripts/src/pulumi/up.ts index 44f8b33..6364378 100644 --- a/.github/scripts/src/pulumi/up.ts +++ b/.github/scripts/src/pulumi/up.ts @@ -8,7 +8,7 @@ import _ from "lodash"; export async function main() { const envClient = EnvClient.create(EnvClientStrategy.SOPS); - const { azurePulumiLocation, env } = parseCiEnv( + const { pulumiBackendUrl, env } = parseCiEnv( _.merge( await envClient.readFromEnv("secrets.yaml"), await envClient.readFromEnv("secrets.administrator.yaml"), @@ -20,7 +20,7 @@ export async function main() { stackName: "main", workDir: ".", envs: { - PULUMI_BACKEND_URL: azurePulumiLocation, + PULUMI_BACKEND_URL: pulumiBackendUrl, ...env, }, }); @@ -42,16 +42,16 @@ export async function main() { } function parseCiEnv(ciEnv: Record) { - const azurePulumiLocation = (() => { - const v = ciEnv["AZURE_PULUMI_LOCATION"]; + const pulumiBackendUrl = (() => { + const v = ciEnv["PULUMI_BACKEND_URL"]; if (!v) { - throw new Error("Missing AZURE_PULUMI_LOCATION from .env.ci"); + throw new Error("Missing PULUMI_BACKEND_URL from .env.ci"); } return v; })(); return { - azurePulumiLocation, + pulumiBackendUrl, env: ciEnv, }; } diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 95efe35..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1,5 +0,0 @@ -* @Patina-Network/infra - -CODEOWNERS @Patina-Network/admin - -.github/ @Patina-Network/cicd diff --git a/Justfile b/Justfile index 5b22c85..dee3c67 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,13 @@ +### Azure cmds + +# logout and authenticate again +re-auth: + az logout && just auth + +# authenticate +auth: + just install-pre-scripts && az login + ### Core Commands # Initial setup command @@ -11,6 +21,10 @@ init-pulumi *args: preview *args: sops exec-env secrets.yaml "sops exec-env secrets.administrator.yaml \"pulumi preview {{ args }}\"" +# Print the initial password Pulumi generated for a newly-created Azure user, keyed by their full email. +get-init-pwd fullEmail: + sops exec-env secrets.yaml "pulumi stack output azureInitPwsPlaintext --show-secrets --json | jq -er --arg e '{{ fullEmail }}' '.[\$e]'" + ### Secret management # sops is a library that handles the encryption and decryption of files (primarily used for secrets). diff --git a/bun.lock b/bun.lock index c9dfd0e..02de311 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@pulumi/github": "^6.12.2", "@pulumi/postgresql": "^3.16.3", "@pulumi/pulumi": "^3.230.0", + "@pulumi/random": "^4.21.0", "@tahminator/pipeline": "^1.0.63", "lodash": "^4.18.1", }, @@ -219,6 +220,8 @@ "@pulumi/pulumi": ["@pulumi/pulumi@3.232.0", "", { "dependencies": { "@grpc/grpc-js": "^1.10.1", "@logdna/tail-file": "^2.0.6", "@npmcli/arborist": "^9.0.0", "@opentelemetry/api": "^1.9", "@opentelemetry/exporter-trace-otlp-grpc": "^0.57", "@opentelemetry/exporter-zipkin": "^1.30", "@opentelemetry/instrumentation": "^0.57", "@opentelemetry/instrumentation-grpc": "^0.57", "@opentelemetry/resources": "^1.30", "@opentelemetry/sdk-trace-base": "^1.30", "@opentelemetry/sdk-trace-node": "^1.30", "@types/google-protobuf": "^3.15.5", "@types/semver": "^7.5.6", "@types/tmp": "^0.2.6", "execa": "^5.1.0", "fdir": "^6.5.0", "google-protobuf": "^3.21.4", "ini": "^2.0.0", "js-yaml": "^3.14.2", "minimist": "^1.2.6", "normalize-package-data": "^6.0.0", "picomatch": "^4.0.0", "require-from-string": "^2.0.1", "semver": "^7.5.2", "source-map-support": "^0.5.6", "tmp": "^0.2.4", "upath": "^1.1.0" }, "peerDependencies": { "ts-node": ">= 7.0.1 < 12", "typescript": ">= 3.8.3 < 7" }, "optionalPeers": ["ts-node", "typescript"] }, "sha512-5Pl48cCwOOZEvG7b6w6sErrD1D/QiEwiPqEtHCIzF/alU0yzFjo95uxNteKFlt0LsnzWsZ58DJHgBe9gurjIFg=="], + "@pulumi/random": ["@pulumi/random@4.21.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-k4JHqQFOKXWkbSLZtX37xBlRsurfgZbLd1Zib0cLSDP4Dt3uTllR3CBWfMLSnXJGAoTr9MlwyHCer/5tmSaF0Q=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], diff --git a/package.json b/package.json index efe9563..dad016a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@pulumi/github": "^6.12.2", "@pulumi/postgresql": "^3.16.3", "@pulumi/pulumi": "^3.230.0", + "@pulumi/random": "^4.21.0", "@tahminator/pipeline": "^1.0.63", "lodash": "^4.18.1" } diff --git a/secrets.administrator.yaml b/secrets.administrator.yaml index 0719b6a..7a5e804 100644 --- a/secrets.administrator.yaml +++ b/secrets.administrator.yaml @@ -1,11 +1,14 @@ ARM_CLIENT_ID: ENC[AES256_GCM,data:qbcZ5yNddBGlbC/TlDc0BoQiz4SDpNaoTzldrbsnXClHzfHW,iv:/YAAPO0sYtjQsLIqCtY087+E9hPdDqK+aOWRDz0Srug=,tag:KZxyGpyeec9BL+pbSlqHkQ==,type:str] -ARM_CLIENT_SECRET: ENC[AES256_GCM,data:sCvNWJ5vt6HT4nOja03B5INpt2Hvm7KaJY8A5Bm7a4DBXbZZbGdvig==,iv:MHNxA4/Hus1TclqsTsrHYfxNDT6gkqpawwqO941po8Q=,tag:5U3B26ufzCClfuOQvdkq1w==,type:str] +#ENC[AES256_GCM,data:BH8gEurU+rmXUesa9l/FMAY8Gl2ZcVmu2UWfF9KlkHATYv3WWV7rttkUUERKrtKWdqJOYzpmlbb3M0CY/CcH7q4YWNlbZC0qjYI1iiAvYCSCuyvDDaAdabRFw4TN3tnW0Q==,iv:6p9UliuKaba6/0f8VjsxjPCcnsznDrkhHW8etHs6500=,tag:ZwxyiOskFaUTkAnX4DtbSw==,type:comment] +#ENC[AES256_GCM,data:wZT3oJBwFONQf2COI+DYwGnmDrp2w8DnITAZtzz83Jh2sk/9HnK97nHOYa4ApSuzs1Yd+7qNAgCU/Jh7eSKe3BQ8lJr9Vx5gR91PkDTHao91f+d3AHf1ktx7aQjQvUj3ttY=,iv:wiUPcQhEDfI2e4tG6l/2nWoGFix0ihldFgNZl9Vxfic=,tag:jSFVNMD7qpYssV+zR45sQQ==,type:comment] +ARM_CLIENT_SECRET: ENC[AES256_GCM,data:X6Ll4sHd5Ycj+A4dPNDjQpL99ewP+61TIAt68yNo3kcRPqDYsOi4vw==,iv:WSl3/UzBLvC94q+OlsvD6iY6pSI5lls2y2p9POD6ZSM=,tag:OpGnj5eXhZh1gy/7n9TFwA==,type:str] ARM_TENANT_ID: ENC[AES256_GCM,data:96BqORRYTbL8HApe+v7ZRgnXvr7WHoASttvyCaeOCTJsuQPG,iv:EO++192dYNcjU83sEdvdTpqF7dfUZskJmLAMkioowXA=,tag:vO14lQdLkKVQIbcaCQ93Nw==,type:str] ARM_SUBSCRIPTION_ID: ENC[AES256_GCM,data:axfnnc6Yo24TRsgmOig7VATBOO7s87Vo2yy/LOsJHn1KoI1R,iv:QOq/ocMxabsq0l89t8hZ6gSbDbvNMzDNiXwgKNdJ9l4=,tag:0rYMbIVCqzGjHYTg+c3Cxw==,type:str] AZURE_CLIENT_ID: ENC[AES256_GCM,data:UzzZgyrrsGDgmH49MWTBC0/lzxlUxP8ABwH6wjvt0FIfuh4g,iv:MRnISPgmYoc8eFn7LXP6H4mDEWXuNOEcTb2kUg/1M5k=,tag:d9eJBetuEF3RTELSgyQOVQ==,type:str] -AZURE_CLIENT_SECRET: ENC[AES256_GCM,data:Ihx1go1jvZd61nb4bY+Vwyuf4g6U4fompgjR96XD+2W5g7TC2ySqvg==,iv:oasm3BPBVMuKfMY6Pw0WU3J1hXaGLF5MiYgxfIqfNiI=,tag:QLTJkkCWNCz+i5ayxEzhrg==,type:str] +AZURE_CLIENT_SECRET: ENC[AES256_GCM,data:IFvb59Gk/Hvr2LKef5jBwPeDWzjaI6E/Z5JNwWwZJ/X/zbk7F7h+/Q==,iv:0Gg6HOstjRory+JiNlKyPc1tsGl6BejSXAXp+JFMiHM=,tag:A1atnwbNn/hiOdFQ6AJXqQ==,type:str] AZURE_TENANT_ID: ENC[AES256_GCM,data:dL69CpG0P/7uNSkbs4iWEfwEepyFDHYvKYWQb/+DMpKCNXRz,iv:BAPf7vO3W9j7GdNT9N3bN2MJmlHgzBIU77/oVGgf+DY=,tag:kIBbL4PDn4KgJTNYSNfugg==,type:str] -AZURE_PULUMI_LOCATION: ENC[AES256_GCM,data:HSJ4R+0typDoqPlvuiKxG0lag7u2C4eGTfmRg/349ZF1UAeOn0JGh6PMSCWct/agpuWv+qg=,iv:1NAH2+zZvVUtiuQrYg7soXFUBOU5ISJyL+zv6WIggoM=,tag:p2ZtLebf6Iu5Ivw2DEjPXQ==,type:str] +PULUMI_BACKEND_URL: ENC[AES256_GCM,data:ndLguPQ8Qvl4iT5B/YNA8AASikOc06URFCZuXjDJtsIlsvAhEwW2pIgmCkVggEBuC2dTfi0=,iv:bURpxt5L9x5eG+PX8IEre1UBxKlIL62DjmI9Bfy4ZHE=,tag:DyI80XTWueqLADw3SiFeCw==,type:str] +DIGITAL_OCEAN_DNS_PAT: ENC[AES256_GCM,data:POILwhJDKuQCiHyucptyDck/aIzXlrgtvwsiauaF3stTH865El/zgKRWKqS2xMY2tlCoPamEVF5fC0ffa9MXWMLcR9zK0Gg=,iv:Rz9MmwOjPtyrTs8W3TsljZv5M/fRukecX2fdHKPhcGU=,tag:zylfzsT4HUQqw8qBSxkPfw==,type:str] sops: azure_kv: - created_at: "2026-07-02T22:29:03Z" @@ -13,7 +16,7 @@ sops: name: sops-administrators-key vault_url: https://sops-administrators.vault.azure.net version: 37bc81e975ea4a2194985560d066d52d - lastmodified: "2026-07-02T22:29:04Z" - mac: ENC[AES256_GCM,data:oI2BmQDHSdEOyJdN+buUm08hMjH2k4B6Kcfkm2rPjJIT6JNjTF1LVxQ6lK3VzYrWYqSl5YcMLcerkTEsD+WU694J2LBvNggfw3FKxjoz/michL5B0qyxovca6kAJ6ho+a1/3GOLR6V1dNcz8JyPFOZu+wDPYfnZyZIWQvf+njkQ=,iv:fJJQ+LsCVowQPYW8UjcwDuK0hPxpwKiRBCkD8jjPdgY=,tag:FW4XvGurDaXM9gBI1Rde4g==,type:str] + lastmodified: "2026-08-01T18:26:52Z" + mac: ENC[AES256_GCM,data:u1PB/9gCbPg8ZSL5e9C/dr9JfZ3NrCpbkJNzMirMUzgO6fntahsJatu0IWZ8B69AVfO8+DVNts0e8tbjhEAWWkQxmWJPTs2RZTE+TlvB2jszbMtaS8CLvuAF6exT1iSQmCF7E1t8huE8O5cyK9LFW705vdHTbdBWtrtDrtU9iIU=,iv:5MKl0uAusv7b1fuhEAhRs5jOH0TnjJr1ApQ5POlDNSw=,tag:/dqlgJ/iVqpcHbjleQ73tA==,type:str] unencrypted_suffix: _unencrypted - version: 3.13.1 + version: 3.13.3 diff --git a/secrets.yaml b/secrets.yaml index 769a91f..dc4bc15 100644 --- a/secrets.yaml +++ b/secrets.yaml @@ -21,6 +21,7 @@ PG_ROLE_patchats-stg-app: ENC[AES256_GCM,data:x7K16UjqGLbYL8hE8eiE00BUAJLyCTqwwo PG_ROLE_patchats-prod-app: ENC[AES256_GCM,data:T+Deyb6OVwHCuimUv6U2C6eG2u9zi9gYTdV95p+wykY=,iv:GdFQCqD8ceCCCdzO08mkTd80yASrVBsmPPKy+xVzV/A=,tag:gdJ6jsY5lRRlryTzfB1f+w==,type:str] PG_ROLE_patchats-stg-ro: ENC[AES256_GCM,data:jEqekXU41TceQtTKErilTO1MmaExFun+9inpre9KpQA=,iv:DLrr1/pBLaHa/WwvAZrT/M4VxzYGGO7QbingkuyZqzk=,tag:SjmnWZccaXjARNQYvx9NWg==,type:str] PG_ROLE_patchats-prod-ro: ENC[AES256_GCM,data:8J8a6LoIcBWjGAw5ZmgH1J4Nyybc9NuC5A4LMaGpKgY=,iv:JaifCIri3ZoQgmQok4mtAYGNNhEGWsQa8fCXrpRhyyU=,tag:1Awikh04S5ExCLur5hvIjw==,type:str] +PG_ROLE_grafana-all-ro-sa: ENC[AES256_GCM,data:7SWq7juAA+XKWY4KYWeE8Q11suy7pnEvjQCeiGY+HjQ=,iv:bhMtmaA8PMgREtVYEIRY++nIlHRc2y24nnLFDBSSEyQ=,tag:zaE7eL3m5fdrwOpZvb3+aw==,type:str] sops: azure_kv: - created_at: "2026-04-23T06:04:34Z" @@ -28,7 +29,7 @@ sops: name: sops-key vault_url: https://sops-master.vault.azure.net version: 90784f2986de4514934b4d1f682d3e59 - lastmodified: "2026-07-02T22:20:58Z" - mac: ENC[AES256_GCM,data:8V/0EstscuMzjHxzcNlrooTrx8ScgKTQ1ieaYHAdkiaayDsKiGwhcW9SVPow2mXG2o2iOan5dhVGsqYVXLkLRaYqL0VezmADweR0xCfscfzDlhx/TKBOL6lOoi1OD23JBT+VaIFFSMYpG724ucg7U7KQwiYiMuyFCF/fy5sMdqI=,iv:bLZmO9rfNqim7MJ628Iop3nuIOZvkpS+IjFyACp3j6w=,tag:BgU3ddnjBTWfmzE/Jm31Lw==,type:str] + lastmodified: "2026-07-26T03:13:59Z" + mac: ENC[AES256_GCM,data:HlcOLXSYw85K0TDLBO60APZ4qp/1rdl5B34c5ANbSoeeOYXtBfpSilIUyIhLPwz77MXvvcNLBn6JzYuasOi0Vd2i82EP0Y265Wkq1k3MAHyo58VZWj1smHjUVKVGfK21GLMduye5AuLBNrhVbQHVvY9zuHmswu7v+p9F+9Q2Tyc=,iv:AHIzq8ez/R7MaLmYRJzheWZJop6alLrrKQYHtrVScog=,tag:m4+WPyHMkFIxfK+C+0W3SQ==,type:str] unencrypted_suffix: _unencrypted version: 3.13.1 diff --git a/src/azure/users/index.ts b/src/azure/users/index.ts index 00045c4..1c09db6 100644 --- a/src/azure/users/index.ts +++ b/src/azure/users/index.ts @@ -1,10 +1,11 @@ import * as azuread from "@pulumi/azuread"; +import * as pulumi from "@pulumi/pulumi"; +import * as random from "@pulumi/random"; import { azureadProvider as provider } from "@/azure/provider"; import { AZURE_USERS } from "@/azure/users/inputs"; import { split } from "@/utils"; -const getUserImportId = (objectId: string) => `/users/${objectId}`; const getUserResourceName = (firstName: string, lastName: string) => `azure-user-${firstName.toLowerCase()}-${lastName.toLowerCase()}`; @@ -13,43 +14,89 @@ const fullName = ( lastName: LS, ): `${FS} ${LS}` => `${firstName} ${lastName}`; -export const azureUsers = Object.fromEntries( +export const azureInitPws = Object.fromEntries( Object.entries(AZURE_USERS) .map( - ([fullName, _]) => + ([fullName, u]) => [ { firstName: split(fullName, " ")[0], lastName: split(fullName, " ")[1], }, - _, + u, ] as const, ) - .map(([{ firstName, lastName }, user]) => [ - fullName(firstName, lastName), - new azuread.User( - getUserResourceName(firstName, lastName), - { - displayName: fullName(firstName, lastName), - givenName: firstName, - mail: user.mail, - mailNickname: user.mailNickname, - surname: lastName, - userPrincipalName: user.userPrincipalName, - }, - { - provider, - ignoreChanges: [ - "forcePasswordChange", - "otherMails", - "usageLocation", - "preferredLanguage", - ], - import: - user.bootstrapObjectId ? - getUserImportId(user.bootstrapObjectId) - : undefined, - }, - ), + .map(([{ firstName, lastName }, user]) => { + const pw = (() => { + if (user.newUser) { + return new random.RandomPassword( + `azure-user-init-pw-${firstName}-${lastName}`, + { + length: 20, + special: true, + }, + ); + } + + return null; + })(); + + return [user.mail, pw]; + }), +); + +// so we can read output in pulumi state +export const azureInitPwsPlaintext = pulumi.secret( + Object.fromEntries( + Object.entries(AZURE_USERS).map(([_, user]) => [ + user.mail, + azureInitPws[user.mail]?.result, ]), + ), +); + +export const azureUsers = Object.fromEntries( + Object.entries(AZURE_USERS) + .map( + ([fullName, u]) => + [ + { + firstName: split(fullName, " ")[0], + lastName: split(fullName, " ")[1], + }, + u, + ] as const, + ) + .map(([{ firstName, lastName }, user]) => { + const _fullName = fullName(firstName, lastName); + + const pw = azureInitPws[user.mail]; + + return [ + _fullName, + new azuread.User( + getUserResourceName(firstName, lastName), + { + displayName: _fullName, + givenName: firstName, + mail: user.mail, + mailNickname: user.mailNickname, + surname: lastName, + userPrincipalName: user.userPrincipalName, + password: pw?.result ?? undefined, + forcePasswordChange: user.newUser ? true : undefined, + }, + { + provider, + ignoreChanges: [ + "password", + "forcePasswordChange", + "otherMails", + "usageLocation", + "preferredLanguage", + ], + }, + ), + ]; + }), ); diff --git a/src/azure/users/inputs.ts b/src/azure/users/inputs.ts index 851299a..171c7fd 100644 --- a/src/azure/users/inputs.ts +++ b/src/azure/users/inputs.ts @@ -5,36 +5,17 @@ import type { AzureGlobalRbacRoleName } from "@/azure/users/rbac/const"; * __ATTENTION__: Please follow these instructions in order to onboard a new * Azure user. * - * 1. Log into Azure at `http://portal.azure.com/` - * 2. Head to the `Users` resource - * 3. Click the `New user` button -> `Create new user` - * 4. Fill out any basic required information - * - User Principal Name - * - Mail Nickname (sync with User Principal Name) - * - First Name - * - Last Name - * - Display Name (must be `${firstName} ${lastName}`) - * - Any other personal information is optional, you may fill it out if you choose - * 5. SKIP ASSIGNMENTS, they will be done later (Entra roles should not be assigned to new users either). - * 6. Leave the random password alone (do not forget it / copy it to your clipboard, you must give it to the user; they will be required to change it after logging in) - * 7. Create - * 8. Wait for the user to be created - * 9. THEN, find that `User` resource you just created & copy the `objectId` - * 10. Add the user to `AZURE_USERS` below - * - The key is their firstName + lastName - * - Copy all other information you filled out above that the object requires - * - Put `objectId` inside `bootstrapObjectId` - * 11. Create a PR and ask someone from `@Patina-Network/infra` to review & merge the changes. - * - View the Pulumi diff inside the PR; it will indicate what accesses the user will receive once merged. - * 12. Wait for the changes to be merged and successfully reconciled by Pulumi (check CICD inside the latest tag generated by your PR) - * 13. Once step 12 is complete, come back - * here and set the `bootstrapObjectId`. Pulumi can manage this state without it & it is safe to remove. - * 13. Follow step 11 and 12 + * 1. Add user to `AZURE_USERS` below with `newUser` set to `true`. + * 2. Create & merge PR. + * 3. After state is reconciled, you can run `just get-init-pwd {fullEmail}` to get their initial password. + * Provide their `fullEmail` and this password to them so they can login & change their password when prompted. + * + * __Note__: If you need a password reset, please reach out to a user with `globalAdministrator` or `userAdministrator` role. */ type AzureUser = { - /** Set this to objectId if you are importing a user into Pulumi. Then once state is reconciled, delete it. */ - bootstrapObjectId?: string; + /** Set this to `true` if you are creating a new user. Then once state is reconciled, please set it to `false`. */ + newUser?: boolean; /** Roles giving permission for Microsoft Entra, which manages access to Microsoft's suite of apps, but not Azure. */ entraRoles: readonly AzureGlobalEntraRoleName[]; /** Roles giving permission for Azure, allowing developers to view and manage resources. */ @@ -53,15 +34,15 @@ type AzureUserFullName = string; // Then, we can use Pulumi to wire up resources & permissions. export const AZURE_USERS = { "Henry Chen": { - bootstrapObjectId: undefined, - entraRoles: ["globalAdministrator"], + newUser: false, + entraRoles: ["globalAdministrator", "userAdministrator"], azureRoles: ["owner"], mail: "henry@patinanetwork.onmicrosoft.com", mailNickname: "henry", userPrincipalName: "henry@patinanetwork.onmicrosoft.com", }, "Tahmid Ahmed": { - bootstrapObjectId: undefined, + newUser: false, entraRoles: ["globalAdministrator", "userAdministrator"], azureRoles: ["owner"], mail: "tahmid@patinanetwork.onmicrosoft.com", @@ -69,15 +50,15 @@ export const AZURE_USERS = { userPrincipalName: "tahmid@patinanetwork.onmicrosoft.com", }, "Kevin Ma": { - bootstrapObjectId: undefined, - entraRoles: ["globalAdministrator", "userAdministrator", "globalReader"], + newUser: false, + entraRoles: ["globalAdministrator", "userAdministrator"], azureRoles: ["contributor"], mail: "kevin.ma@patinanetwork.onmicrosoft.com", mailNickname: "kevin.ma", userPrincipalName: "kevin.ma@patinanetwork.onmicrosoft.com", }, "Ray Zhou": { - bootstrapObjectId: undefined, + newUser: false, entraRoles: ["userAdministrator", "globalReader"], azureRoles: ["contributor"], mail: "ray@patinanetwork.onmicrosoft.com", @@ -85,7 +66,7 @@ export const AZURE_USERS = { userPrincipalName: "ray@patinanetwork.onmicrosoft.com", }, "Arshadul Monir": { - bootstrapObjectId: undefined, + newUser: false, entraRoles: ["globalReader", "userAdministrator"], azureRoles: ["contributor"], mail: "arshadul@patinanetwork.onmicrosoft.com", @@ -93,61 +74,85 @@ export const AZURE_USERS = { userPrincipalName: "arshadul@patinanetwork.onmicrosoft.com", }, "Haoking Luo": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "haoking.luo@patinanetwork.onmicrosoft.com", mailNickname: "haoking.luo", userPrincipalName: "haoking.luo@patinanetwork.onmicrosoft.com", }, "Randy Dean": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "randy.dean@patinanetwork.onmicrosoft.com", mailNickname: "randy.dean", userPrincipalName: "randy.dean@patinanetwork.onmicrosoft.com", }, "Isabella Lam": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "isabella.lam@patinanetwork.onmicrosoft.com", mailNickname: "isabella.lam", userPrincipalName: "isabella.lam@patinanetwork.onmicrosoft.com", }, "Allison Lee": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "allison.lee@patinanetwork.onmicrosoft.com", mailNickname: "allison.lee", userPrincipalName: "allison.lee@patinanetwork.onmicrosoft.com", }, "Andrew Yu": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "andrew.yu@patinanetwork.onmicrosoft.com", mailNickname: "andrew.yu", userPrincipalName: "andrew.yu@patinanetwork.onmicrosoft.com", }, "Maliha Tasnim": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["reader"], mail: "maliha.tasnim@patinanetwork.onmicrosoft.com", mailNickname: "maliha.tasnim", userPrincipalName: "maliha.tasnim@patinanetwork.onmicrosoft.com", }, "Ousmane Barrie": { - bootstrapObjectId: undefined, - entraRoles: ["globalReader", "userAdministrator"], + newUser: false, + entraRoles: ["globalReader"], azureRoles: ["contributor"], mail: "ousmane.barrie@patinanetwork.onmicrosoft.com", mailNickname: "ousmane.barrie", userPrincipalName: "ousmane.barrie@patinanetwork.onmicrosoft.com", }, + "Angela Yu": { + newUser: false, + entraRoles: ["globalReader"], + azureRoles: ["reader"], + mail: "angela.yu@patinanetwork.onmicrosoft.com", + mailNickname: "angela.yu", + userPrincipalName: "angela.yu@patinanetwork.onmicrosoft.com", + }, + "Nancy Huang": { + newUser: false, + entraRoles: ["globalReader"], + azureRoles: ["reader"], + mail: "nancy.huang@patinanetwork.onmicrosoft.com", + mailNickname: "nancy.huang", + userPrincipalName: "nancy.huang@patinanetwork.onmicrosoft.com", + }, + "Kelly Lin": { + newUser: true, + entraRoles: ["globalReader"], + azureRoles: ["reader"], + mail: "kelly.lin@patinanetwork.onmicrosoft.com", + mailNickname: "kelly.lin", + userPrincipalName: "kelly.lin@patinanetwork.onmicrosoft.com", + }, } as const satisfies Record; export type AzureUserName = keyof typeof AZURE_USERS; diff --git a/src/env.ts b/src/env.ts index 4ec9d98..ea151e4 100644 --- a/src/env.ts +++ b/src/env.ts @@ -42,6 +42,7 @@ export const env = { "patchats-prod-app": required("PG_ROLE_patchats-prod-app"), "patchats-stg-ro": required("PG_ROLE_patchats-stg-ro"), "patchats-prod-ro": required("PG_ROLE_patchats-prod-ro"), + "grafana-all-ro-sa": required("PG_ROLE_grafana-all-ro-sa"), }, }, } as const; diff --git a/src/github/members/inputs.ts b/src/github/members/inputs.ts index a2facd3..60377cc 100644 --- a/src/github/members/inputs.ts +++ b/src/github/members/inputs.ts @@ -23,6 +23,7 @@ export const MEMBERS = [ { username: "spiffyy99", role: "member" }, { username: "angelayu0530", role: "member" }, { username: "naanci", role: "member" }, + { username: "Kxlcl", role: "member" }, ] as const satisfies readonly GithubMember[]; export type GithubUsername = (typeof MEMBERS)[number]["username"]; diff --git a/src/github/repositories/const.ts b/src/github/repositories/const.ts index 7833888..5fd2232 100644 --- a/src/github/repositories/const.ts +++ b/src/github/repositories/const.ts @@ -6,6 +6,7 @@ export const GITHUB_APP_ID = { githubActions: 15368, sonarCloud: 12526, + patAgent: 3239387, } as const; export const DEFAULT_SONARCLOUD_ANALYSIS_JOB_NAME = diff --git a/src/github/repositories/index.ts b/src/github/repositories/index.ts index 2f88664..ae664f0 100644 --- a/src/github/repositories/index.ts +++ b/src/github/repositories/index.ts @@ -4,13 +4,17 @@ import * as github from "@pulumi/github"; import { GITHUB_OWNER } from "@/github/inputs"; import { provider } from "@/github/provider"; -import { DEFAULT_SONARCLOUD_ANALYSIS_JOB_NAME } from "@/github/repositories/const"; +import { + DEFAULT_SONARCLOUD_ANALYSIS_JOB_NAME, + GITHUB_APP_ID, +} from "@/github/repositories/const"; import { DEFAULT_MAIN_BRANCH_PROTECTIONS, DEFAULT_REPOSITORY_SETTINGS, REPOSITORIES, type GithubRepositoryName, type GithubTeamReference, + type MainBranchProtectionBypassActor, } from "@/github/repositories/inputs"; import { githubTeams } from "@/github/teams"; import { mergeWithConcatArrays } from "@/utils"; @@ -147,6 +151,17 @@ export const githubRepositoryDefaultBranchRulesets = Object.entries( return protections; })(); + const requiredReviewers = repositoryConfig.mainBranchRequiredReviewers.map( + ({ filePatterns, team, minimumApprovals }) => ({ + filePatterns: [...filePatterns], + minimumApprovals, + reviewer: { + id: githubTeams[getTeamName(team)].id.apply(Number), + type: "Team", + }, + }), + ) satisfies github.types.input.RepositoryRulesetRulesPullRequestRequiredReviewer[]; + return [ new github.RepositoryRuleset( getDefaultBranchRulesetResourceName(repositoryName), @@ -155,15 +170,24 @@ export const githubRepositoryDefaultBranchRulesets = Object.entries( enforcement: "active", target: "branch", bypassActors: - repositoryConfig.mainBranchProtectionBypassTeams.length ? - repositoryConfig.mainBranchProtectionBypassTeams - .map((team) => getTeamName(team)) - .map((teamName) => githubTeams[teamName].id) - .map((teamId) => ({ - actorType: "Team", - actorId: teamId.apply(Number), - bypassMode: "pull_request", - })) + repositoryConfig.mainBranchProtectionBypass.length ? + repositoryConfig.mainBranchProtectionBypass.map((actor) => { + const a = actor as MainBranchProtectionBypassActor; + if ("team" in a) { + const teamName = getTeamName(a.team); + return { + actorType: "Team", + actorId: githubTeams[teamName].id.apply(Number), + bypassMode: "pull_request", + }; + } + + return { + actorType: "Integration", + actorId: GITHUB_APP_ID[a.app], + bypassMode: "always", + }; + }) : undefined, repository: repository.name, conditions: { @@ -174,6 +198,11 @@ export const githubRepositoryDefaultBranchRulesets = Object.entries( }, rules: { ...branchProtections, + pullRequest: { + ...(branchProtections.pullRequest ?? {}), + requiredReviewers: + requiredReviewers.length ? requiredReviewers : undefined, + }, }, }, { diff --git a/src/github/repositories/inputs.ts b/src/github/repositories/inputs.ts index 0bffbde..419778f 100644 --- a/src/github/repositories/inputs.ts +++ b/src/github/repositories/inputs.ts @@ -6,6 +6,7 @@ import type { GITHUB_OWNER } from "@/github/inputs"; import { DEFAULT_SONARCLOUD_ANALYSIS_JOB_NAME, GITHUB_APP_ID, + type GithubAppIdName, } from "@/github/repositories/const"; import { TEAMS, type GithubTeamName } from "@/github/teams/inputs"; @@ -19,6 +20,20 @@ import { TEAMS, type GithubTeamName } from "@/github/teams/inputs"; type RepositoryVisibility = "public" | "private" | "internal"; type GithubTeamReference = `@${typeof GITHUB_OWNER}/${GithubTeamName}`; +export type MainBranchProtectionBypassActor = + | { + team: GithubTeamReference; + } + | { + app: GithubAppIdName; + }; + +type MainBranchRequiredReviewer = { + team: GithubTeamReference; + filePatterns: readonly string[]; + minimumApprovals: number; +}; + type GithubRepository = { /** set to `true` when repository has not been seen by Pulumi yet. Set to `false` after Pulumi has successfully reconciled state __AFTER MERGING SAID CHANGE__. */ bootstrap: boolean; @@ -34,7 +49,10 @@ type GithubRepository = { visibility: RepositoryVisibility; repositorySettingOverrides: Partial; mainBranchProtectionOverrides: Partial; - mainBranchProtectionBypassTeams: readonly GithubTeamReference[]; + // teams/humans can only bypass via `PR` (aka cannot directly pass via Git). + // apps however can fully bypass (as they are automated). + mainBranchProtectionBypass: readonly MainBranchProtectionBypassActor[]; + mainBranchRequiredReviewers: readonly MainBranchRequiredReviewer[]; /** if set to `true`, will exclude default `SonarCloud Code Analysis` status check. You are expected to register your own multi-scanner status checks instead. */ monorepo: boolean; }; @@ -58,7 +76,8 @@ export const DEFAULT_MAIN_BRANCH_PROTECTIONS: RepositoryRulesetRules = { requiredApprovingReviewCount: 1, dismissStaleReviewsOnPush: false, requireLastPushApproval: true, - requireCodeOwnerReview: true, + // CODEOWNERS is retired in favor of `mainBranchRequiredReviewers`. + requireCodeOwnerReview: false, requiredReviewThreadResolution: true, }, requiredStatusChecks: { @@ -77,6 +96,22 @@ const ALL_GITHUB_TEAMS = Object.entries(TEAMS).map( ([k]) => `@Patina-Network/${k}` as const, ); +const CICD_REVIEWER = { + team: "@Patina-Network/cicd", + filePatterns: [".github/**"], + minimumApprovals: 1, +} as const satisfies MainBranchRequiredReviewer; + +const k8sAppManifests = (app: string, negate = false): string[] => { + const files = [ + `base/production/${app}/kustomization.yaml`, + `base/production/${app}/application-prod.yaml`, + `base/staging/${app}/kustomization.yaml`, + `base/staging/${app}/application-stg.yaml`, + ]; + return negate ? files.map((file) => `!${file}`) : files; +}; + export const REPOSITORIES = { "k8s-manifests": { description: @@ -90,8 +125,42 @@ export const REPOSITORIES = { triage: [], repositorySettingOverrides: {}, mainBranchProtectionOverrides: {}, - // TODO: remove when initial bootstrapping & prototyping is complete - mainBranchProtectionBypassTeams: ["@Patina-Network/infra"], + mainBranchProtectionBypass: [ + { + team: "@Patina-Network/infra", + }, + { + app: "patAgent", + }, + ], + mainBranchRequiredReviewers: [ + { + // infra owns everything except specific app manifests. + team: "@Patina-Network/infra", + filePatterns: [ + "**/*", + "!.github/**", + ...k8sAppManifests("codebloom", true), + ...k8sAppManifests("codebloom-standup-bot", true), + ...k8sAppManifests("patchats", true), + ], + minimumApprovals: 1, + }, + { + team: "@Patina-Network/codebloom", + filePatterns: [ + ...k8sAppManifests("codebloom"), + ...k8sAppManifests("codebloom-standup-bot"), + ], + minimumApprovals: 1, + }, + { + team: "@Patina-Network/patchats", + filePatterns: [...k8sAppManifests("patchats")], + minimumApprovals: 1, + }, + CICD_REVIEWER, + ], }, "platform-infra": { description: @@ -120,8 +189,19 @@ export const REPOSITORIES = { ], }, }, - // TODO: remove when initial bootstrapping & prototyping is complete - mainBranchProtectionBypassTeams: ["@Patina-Network/infra"], + mainBranchRequiredReviewers: [ + { + team: "@Patina-Network/infra", + filePatterns: ["**/*", "!.github/**"], + minimumApprovals: 1, + }, + CICD_REVIEWER, + ], + mainBranchProtectionBypass: [ + { + team: "@Patina-Network/infra", + }, + ], }, ".github": { description: undefined, @@ -134,7 +214,15 @@ export const REPOSITORIES = { triage: [], repositorySettingOverrides: {}, mainBranchProtectionOverrides: {}, - mainBranchProtectionBypassTeams: [], + mainBranchProtectionBypass: [], + mainBranchRequiredReviewers: [ + { + team: "@Patina-Network/admin", + filePatterns: ["**/*", "!.github/**"], + minimumApprovals: 1, + }, + CICD_REVIEWER, + ], }, patchats: { description: @@ -148,7 +236,15 @@ export const REPOSITORIES = { triage: [], repositorySettingOverrides: {}, mainBranchProtectionOverrides: {}, - mainBranchProtectionBypassTeams: [], + mainBranchProtectionBypass: [], + mainBranchRequiredReviewers: [ + { + team: "@Patina-Network/patchats", + filePatterns: ["**/*", "!.github/**"], + minimumApprovals: 1, + }, + CICD_REVIEWER, + ], }, codebloom: { description: "Codebloom - LeetCode Leaderboard for Patina Network", @@ -161,9 +257,6 @@ export const REPOSITORIES = { triage: [], repositorySettingOverrides: {}, mainBranchProtectionOverrides: { - pullRequest: { - requiredApprovingReviewCount: 2, - }, requiredStatusChecks: { requiredChecks: [ { @@ -201,7 +294,15 @@ export const REPOSITORIES = { ], }, }, - mainBranchProtectionBypassTeams: [], + mainBranchRequiredReviewers: [ + { + team: "@Patina-Network/codebloom", + filePatterns: ["**/*", "!.github/**"], + minimumApprovals: 2, + }, + CICD_REVIEWER, + ], + mainBranchProtectionBypass: [], }, dockerfiles: { description: "Toolsets and software baked into static Docker images", @@ -223,7 +324,15 @@ export const REPOSITORIES = { ], }, }, - mainBranchProtectionBypassTeams: [], + mainBranchProtectionBypass: [], + mainBranchRequiredReviewers: [ + { + team: "@Patina-Network/infra", + filePatterns: ["**/*", "!.github/**"], + minimumApprovals: 1, + }, + CICD_REVIEWER, + ], }, } as const satisfies Record; diff --git a/src/github/repositories/secrets/codebloom.yaml b/src/github/repositories/secrets/codebloom.yaml new file mode 100644 index 0000000..c7efd4d --- /dev/null +++ b/src/github/repositories/secrets/codebloom.yaml @@ -0,0 +1,55 @@ +TZ: ENC[AES256_GCM,data:aN5DoIg3iiwawsupEQeTpA==,iv:SR4aYXLwKALRL3X6ZcfGcPdJI4h5odyi8UK6+OFZEMw=,tag:UzLaUuOdNIUMdLVFXqPTSw==,type:str] +DOCKER_HUB_PAT: ENC[AES256_GCM,data:wW1owWVQNNiw1/PI5zqMoO0GkH97rTtNbutCgwJlsyTKMOyQ,iv:JB5E9ToOqi5q9lBg7jR4b8/og81uIlFvVxQg9CXoeYA=,tag:bqhrzpQagW+5irK2NBkXBg==,type:str] +DIGITALOCEAN_PAT: ENC[AES256_GCM,data:Tw8S7E7VGjpTG8M82+z1OF1XkPkVL/cUjwvaIIMpFIKJxgnmLseeaP+RawDAiQUapSOA5Up//42wnefHfwuRXUZLAEpIqh0=,iv:9lOODulcwh6J3VNKPLPAf1UHdnTqxbPWRhbJK2Ay9B8=,tag:29jeGWfilM45Xp5Am4FqMA==,type:str] +DIGITALOCEAN_PROJECT_ID: ENC[AES256_GCM,data:IfsANTyUBkCvJ1QQ5nWUpnP66JFdz1RSKdvf8mD7SG/EXqJY,iv:O1Kn1xRAhcwKCS33ER/Pjpyn0wZvc12I0tIfBByfA4Y=,tag:foYBcQEOjrjhm5HkcKGJ1g==,type:str] +GH_PAT: ENC[AES256_GCM,data:PVh1GDE9PwAotGN4wnmFIELNqWyJl3j3S4PXQ5crlqgCLkjom2pyFQ==,iv:ksFk62mYL04O0pAsZJqPeQJMYSh6eVLuFL8UtT7va6g=,tag:J6qU4dd89zAmrxznRaAEzw==,type:str] +NOTION_SECRET: ENC[AES256_GCM,data:t8vWFVwzetehw7zZ2S/Zocap+dswP5Sy5qeQ0X+E5KHzbeIyNkOuwwdcbQ0Ibhyzysw=,iv:1Bfx4TIl9sevNE50hcZOYnnmOqMJi/KGyaViKZJQGIc=,tag:3VFDZ9/hlYqz4tLFpGUyjA==,type:str] +NOTION_TASK_DB_ID: ENC[AES256_GCM,data:qaMDgzhf5XUquxObAtzAfNZp1D1ejGAOWGLx1LaTkPEAUsSd,iv:H0mLlH/iXGVDSZcHSmAjc6xfH9P4U7J4TNRE0v50LSw=,tag:mDFy2E8CCPleV153VNhANQ==,type:str] +GEMINI_API_KEY: ENC[AES256_GCM,data:lYdjCGIEIns3lyFZqctbPZawZ6nGTMa/iIaFZ4+QDMy9aAL3yDPv,iv:A92Vd+gPvB3lse/JKrhoOq3u/7CMzGj+cVFU1iHj3ug=,tag:uaBmT5GAKnECPJuGtG2G5Q==,type:str] +SONAR_TOKEN: ENC[AES256_GCM,data:fGXvEo/v+2hwmpOgjI1NeMC16mArikGUm3uDApfSQMTwZSj4HoumOQ==,iv:RuWuCxfOkDxGCBjNrSvKml/Q/ZPizh0CSESLjuCst8E=,tag:WFGzt2lCaA6XyApXuyA92A==,type:str] +OPENSEARCH_USER: ENC[AES256_GCM,data:D0dwxpk=,iv:xbBwNwYKvFfkdXuUfdUTGKPrUC5JR0LLB+gQgjCuFdQ=,tag:US+7pl/Lbpn9xKHD1ykajg==,type:str] +OPENSEARCH_PASSWORD: ENC[AES256_GCM,data:Sxce54nLAr5iG0/PCXnXlH0D5dw=,iv:yyVGO1viai/t3tPYjoqJskjkk4+8hkmpi0lAg/b+n1g=,tag:2Wy98RqkmHmgnDEQeZ1n0w==,type:str] +DISCORD_TOKEN: ENC[AES256_GCM,data:5p6qKl/uQI/sBRbiqPA+SA5UQxYSbk6reYFtzkUJg5YgfVVoDxUQeeUmhYMZ/f56FrJSR4N4msG2dsYr3qySvzP6VD/pvc2T,iv:bYagSklSJQnCXGa4NYnkyAD+USVe/Fjlx9nXzQ8Z3f8=,tag:Y2SWmKdYv1RotDFOXZzbPQ==,type:str] +DISCORD_GUILD_ID: ENC[AES256_GCM,data:tgQq5uMSczbBhMjmYXsKbKow2A==,iv:qF8xPI7zutuKpuW7Xqmj+kn2fVULjAypSfm5I7bMKSI=,tag:Y3FBt+vyir1smdHysBTs9w==,type:int] +DISCORD_CHANNEL_ID: ENC[AES256_GCM,data:QXWIdGKMw/B/WVu9FedHMYcqKw==,iv:Wzv35Lko51UZ/xuHfcPpcGGuCAV0J6cHzYKCTplY5gw=,tag:4IlopSV4Hhr61z6Y9OvSwg==,type:int] +CI_APP_DISCORD_CLIENT_ID: ENC[AES256_GCM,data:qO9tJ2tfTYD9SmouM6gTvBVI2g==,iv:ilD0xc+7dYHS/kl/Tf3jn6kk546DO6s9TRyXsyFs4vU=,tag:JLt9ynCEYHfvodDF27S3AQ==,type:int] +CI_APP_DISCORD_CLIENT_SECRET: ENC[AES256_GCM,data:1d/Z6QikMRefjTrxY1k6uF//dKycnu4K6PuIrl+jfjc=,iv:fltNp5AvhK7xZ2H2zoyut+lAOXPo3xb/7a1oKvvSpNM=,tag:qnwJ0snG4C8OH+2SLAFxvg==,type:str] +CI_APP_ENABLE_DOCUMENTATION: ENC[AES256_GCM,data:vb+weg==,iv:XLRgtfzwPG+TxcbcM8WwF6yWVPYRYGfqsCfGV94REWU=,tag:dVlRB6cdXdyiOhqoWWG1WQ==,type:bool] +CI_APP_GH_USERNAME: ENC[AES256_GCM,data:/mwoWSzybxWvhkGdtfg=,iv:UQGeEq/MOSHqrz/3EpnfeS08BiaFJwrSXnoo0fgKW+g=,tag:OuwtY4xgLcG1jCkJ6suShQ==,type:str] +CI_APP_GH_PASSWORD: ENC[AES256_GCM,data:deT6o0U268vLDifoAXU=,iv:HY45SwDEOvhzAELa2hIRsYudNj87DzNfAQPauKc1Bsg=,tag:wPclKsUR7f+7muuFwDN3wA==,type:str] +CI_APP_DISCORD_TOKEN: ENC[AES256_GCM,data:ChRWoJujV+8Otb7dWy5tB1o1ud2QuvE4YCnbV+PISsojGU49H5pNkuRsSePjUqeCpZH9/oTilwvufbln8xV/2nY+G2ZVrG/8,iv:kvsezwyrlXJor1ZWxpVx0Yx70t9vrsfWAYIDD6jWDhA=,tag:zLgzVZ3PQ+Pt7owCVsSvzg==,type:str] +CI_APP_DISCORD_PATINA_GUILD_ID: ENC[AES256_GCM,data:uKy/CTp4/uBgw1TesiLrxiADcA==,iv:RCtIyYfSS3EcTo+ld7QXJ187EBSKONPZle5zgGAluS8=,tag:zcLZnVBNynPifE4LR1dg8g==,type:int] +CI_APP_DISCORD_PATINA_LEETCODE_CHANNEL_ID: ENC[AES256_GCM,data:nuhePigWQpD79TdOfwDirAG+mg==,iv:2hi6BV24tr9nGJUiIOaSIvtL+JG1zH7v4rxaM0winXE=,tag:TSCpvSxkRQ9GTsnV4AAqqQ==,type:int] +CI_APP_DISCORD_REPORTING_GUILD_ID: ENC[AES256_GCM,data:66q4H4VkO2r5Cau2DhwNekzWdg==,iv:8wIDVk4snbi4UEFd3jx9u1RLszNrw4UI3xovAnvL3BQ=,tag:xH0lD33zFNzyddkOmcFgeg==,type:int] +CI_APP_DISCORD_REPORTING_CHANNEL_ID: ENC[AES256_GCM,data:+lPvaLdmfDdzzY+qmCJk+Npn9A==,iv:uFyJQLNhAoOmvswUqb8G9ubKuUarZyoSA4Gyq8PMz3s=,tag:MVP7GHU52e9/VWPIgaIrSQ==,type:int] +CI_APP_DISCORD_REPORTING_LOG_GUILD_ID: ENC[AES256_GCM,data:B1X70vYtFy5Vy2bo0/YLioJRog==,iv:yjUbanA9UOoqBFgsxi59ulb/tlRaB/zjPc+6QOy9hd8=,tag:UjYa6rEz+zM1yZFme/JNvw==,type:int] +CI_APP_DISCORD_REPORTING_LOG_CHANNEL_ID: ENC[AES256_GCM,data:EXZC9XBbDt2jZS3HoClnULm4UA==,iv:r5ZYNpZSCn5pIHwAh0jF/Mgm+WSqzuczkcdt/wb4NJU=,tag:xcHcxTitxN196D0k1QdN6A==,type:int] +CI_APP_GH_EMAIL_HOST: ENC[AES256_GCM,data:OmjqduTJV6hqTtiAPLA=,iv:Gze6vqkvtkUjluFVCkqXXY6kxGB2xsYAHUrOXZCOTnQ=,tag:9RgOlFm87HSo8LG/FtE82Q==,type:str] +CI_APP_GH_EMAIL_PORT: ENC[AES256_GCM,data:jpEL,iv:ElCNhxg75TwTFWoTPbJyBBpSVYZU063koHb+jRmntw4=,tag:QlF5J6fyQ5O5ujk8swuPtA==,type:int] +CI_APP_GH_EMAIL_TYPE: ENC[AES256_GCM,data:Pzjgng==,iv:kcw3UDRsq2BwtP0MwmqA9lO//tNF3wh+yFoRKQJS2PE=,tag:SaAxbDM60a7KMlwen/spfw==,type:str] +CI_APP_GH_EMAIL_USERNAME: ENC[AES256_GCM,data:D8CaQNhCCs3SGfecjlkSM1J8ujiNN2U=,iv:4pkGl3NThVpyjLV2X0I70FP/0nhRM1eZol4tQOWAu20=,tag:T9TS/WMM50ybujVVO3yoPw==,type:str] +CI_APP_GH_EMAIL_PASSWORD: ENC[AES256_GCM,data:Qe81tLPkIFqJ6GtVI1CPUDmWVg==,iv:PTAv/Dn8btTXuz8bmTAfnvxtV9FLwwe00e+Ga6MB4Lk=,tag:IJomjwr4/eo9KAayCl1XIg==,type:str] +CI_APP_CODEBLOOM_EMAIL_HOST: ENC[AES256_GCM,data:o85V3IUX6iDADddMa+A=,iv:OBO5ZQL37ekCiY4XXCKcO9Ms7XeoMyD4aSbhX7vpSjo=,tag:/mbfo1Z/M7ur4d8fBF4nhw==,type:str] +CI_APP_CODEBLOOM_EMAIL_PORT: ENC[AES256_GCM,data:YcdE,iv:RRrMWjmr0Vv/P9a8fNWuOM9Ylrm6aeT8MN+ZPefW3zQ=,tag:tqKxL+Y0T85BO5AcIAdW7g==,type:int] +CI_APP_CODEBLOOM_EMAIL_TYPE: ENC[AES256_GCM,data:jCrMhA==,iv:MmGXbNutjIFMtE+WbmFtRS4zobxiAuZ0fG10/7mZOHY=,tag:v9qKuawUUcUP1ZpwUQzJlw==,type:str] +CI_APP_CODEBLOOM_EMAIL_USERNAME: ENC[AES256_GCM,data:jcfE+kBQ+9WfQfKqueAb6NQA5Jc6mVQ=,iv:yj+0foEG5ZYEUdK8wxuUWTyIlkGG2VpMopo09uwRUvg=,tag:A0Z85xkYgRQEfWwzq2jn+Q==,type:str] +CI_APP_CODEBLOOM_EMAIL_PASSWORD: ENC[AES256_GCM,data:IbrX/IRx4Q6bho1RixR+HiC54g==,iv:E8FG0YTeftMnVPE/qeGQB6TaXRaqob7LijATLHhOyuo=,tag:LlZbghKrLmEurrqAAXDjbg==,type:str] +CI_APP_ACTUATOR_USERNAME: ENC[AES256_GCM,data:WATul80QZMw=,iv:xk9SMaL5pQcUuAI+bTalSKjfVb8qtLeGWyAP9cXPlGk=,tag:zuHx4yeeu5re416Cm34rSQ==,type:str] +CI_APP_ACTUATOR_PASSWORD: ENC[AES256_GCM,data:4J09RntVKjR7Fcj8mOkyDdyw+UoI/prfWN/yak7U+GYyyo8oGJybuwm3CVpdiDctGQBJnSAHdGH9KzbAWT8HIA==,iv:cYBaFiONeJWDHVga+MsCRbP82dh6yqIhYrQoZMt1DEU=,tag:eGySLkG1gC5bBGNFZG7hdA==,type:str] +CI_APP_SERVER_URL: ENC[AES256_GCM,data:3D671Xj2opAOU3EoyBRU3ptRg1JE,iv:MBzZDDlJOIUQKmLq1FAsOluy89Tl4Is74M3eKTeeekM=,tag:8MhbkiPsRRfL6uzuuLBW6Q==,type:str] +CI_APP_REDIS_URL: ENC[AES256_GCM,data:6wJuRm38Z+wPOeip4rSpBGR0Kob9kPZyakFzFCB6ie8GEJH/D0QVHn+cICGnJcyQo5tJP/unLN6nCdyghgi2wlgdFIKghFIbxfMSVNyMlQ4m+TZZ9yghtdfNZwcZw/FYkTJNl0np,iv:dms7pzyRvSBX/4GOMorP3MOq/tIe2xGT4FhtNxmA1pw=,tag:m7D2qt7+w5liejtv+FvMNA==,type:str] +DB_MIGRATIOR_DATABASE_HOST: ENC[AES256_GCM,data:Ci4ySP0Atb5yDPcXrMvvctWUZVyq2aOj,iv:EMV8yK9KTtZP7lWdqIPsXAj0H4NQK4iDC/aQTTW9D2w=,tag:cDY2iuED1MwV9Ek2JUrPTA==,type:str] +DB_MIGRATIOR_DATABASE_PASSWORD: ENC[AES256_GCM,data:HKuiZCLWv20ZbY9Cp4R+PZl8zByLuXCcwl9oB8YtntE=,iv:DVBBZAXXyVCMOH6yVRHM2r8g4XHhiAduziIZU1TubuI=,tag:cHwp6XKCXuRmnLiVql4iOw==,type:str] +DB_MIGRATIOR_DATABASE_PORT: ENC[AES256_GCM,data:D28oQw==,iv:MvY2prT08EjdBLLxxENgrTu4cz5mwg1wcBjtxXUDcys=,tag:UqCriB4KkVV+d/xppaz2CA==,type:int] +DB_MIGRATIOR_DATABASE_USER: ENC[AES256_GCM,data:UGY6JcMqj2TilrrJ,iv:0VoDdCRlW1FtBSL0RqhNb0OrEMPT5TC+CSOrt5o5L/o=,tag:KqxvhIdxCdBrvOAwKhWI+g==,type:str] +sops: + azure_kv: + - created_at: "2026-07-31T03:35:50Z" + enc: lwpVgX-zcWNwJyZjcmlitH4Z-QRYAQjbzxpafvf7yK1rZOkNHsmYxUQDogMgexoQJcL7Lan0gpPy-Vvp7gN6EohXcDm92YxdGGUHCY940JZ9N7SKTylKy_DTJVwNxqxOIhraC5_L9OVYO5Cplu4e13zdCyEeec_-cAtG0V7Bh7iLxKMs2Uem1a6Yyy3PeAPF7MrlLHKRncCyEQ_wa8FecBnGNuz_Jv-pVA6kk0KqK8x8a7AIu8gKFBZvIk-AyZdjj7De7WeIfb3EU-VQ0ApHe51Pzp_jRuRqEmpIEXgoWHfH8pR5PQradkIk1DSSEvDLbgoytXm_jhdm08GcxdHvpQ + name: sops-key + vault_url: https://sops-master.vault.azure.net + version: 90784f2986de4514934b4d1f682d3e59 + lastmodified: "2026-07-31T03:35:53Z" + mac: ENC[AES256_GCM,data:1nTedyXcRsInTaW7jLqSZk64bFizYtM7m+VZ7uujVxSPsjxcphRpHzaa9OjL1nyOL529Vc/08cJxNy4CuFcoOWLLKxQTO5Ly4fgRc6W/A2NjyvkP/cUROYzZPiaktFP9WqtIjWDD8H3spPg4MIOAYSg4Je/f/3a9P8GAotZGDiY=,iv:Fvcc8d0xA5mVsDVjKJNY2/MDYV+e8uHZII/sMY5J3PE=,tag:49TF2iGqct9XJj1y4TPtaw==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 diff --git a/src/github/teams/inputs.ts b/src/github/teams/inputs.ts index 55ded75..ecfc5e6 100644 --- a/src/github/teams/inputs.ts +++ b/src/github/teams/inputs.ts @@ -30,7 +30,7 @@ export const TEAMS = { privacy: "closed", description: "Active team members for the Codebloom project", members: [ - { username: "tahminator", role: "member" }, + { username: "tahminator", role: "maintainer" }, { username: "angelayu0530", role: "member" }, { username: "naanci", role: "member" }, ], @@ -49,6 +49,7 @@ export const TEAMS = { { username: "MalihaT111", role: "member" }, { username: "luoh00", role: "member" }, { username: "spiffyy99", role: "member" }, + { username: "Kxlcl", role: "member" }, ], }, infra: { @@ -57,6 +58,7 @@ export const TEAMS = { members: [ { username: "tahminator", role: "maintainer" }, { username: "arklian", role: "maintainer" }, + { username: "spiffyy99", role: "member" }, ], }, cicd: { @@ -65,6 +67,7 @@ export const TEAMS = { members: [ { username: "tahminator", role: "maintainer" }, { username: "arklian", role: "maintainer" }, + { username: "spiffyy99", role: "member" }, ], }, admin: { diff --git a/src/index.ts b/src/index.ts index 582be98..1f67c51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,6 @@ import "./patches"; import "@/azure"; import "@/github"; import "@/postgres"; + +// add explicit variable outputs to pulumi here (e.g. if you need to be able to see outputs via CLI) +export { azureInitPwsPlaintext } from "@/azure/users"; diff --git a/src/postgres/databases/inputs.ts b/src/postgres/databases/inputs.ts index a35124f..422723c 100644 --- a/src/postgres/databases/inputs.ts +++ b/src/postgres/databases/inputs.ts @@ -17,7 +17,10 @@ type PgDatabase = { }; // applied in index.ts and merged into all databases -export const RO_ALL = ["db-backup-ro-sa"] as const satisfies PgRole[]; +export const RO_ALL = [ + "db-backup-ro-sa", + "grafana-all-ro-sa", +] as const satisfies PgRole[]; export const DATABASES = { "codebloom-stg": { diff --git a/src/postgres/roles/inputs.ts b/src/postgres/roles/inputs.ts index 6808a22..9a0ddc1 100644 --- a/src/postgres/roles/inputs.ts +++ b/src/postgres/roles/inputs.ts @@ -49,6 +49,12 @@ export const ROLES = [ login: true, connectionLimit: 12, }, + { + name: "grafana-all-ro-sa", + password: env.pg.role["grafana-all-ro-sa"], + login: true, + connectionLimit: 12, + }, { name: "db-backup-ro-sa", password: env.pg.role["db-backup-ro-sa"],