@fortytwoservices/ai-universe-setup 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/app.bicep +145 -0
- package/assets/docker-compose.yml +50 -0
- package/assets/main.bicep +233 -0
- package/assets/modules/auditSecret.bicep +28 -0
- package/assets/modules/backendApp.bicep +107 -0
- package/assets/modules/containerApp.bicep +99 -0
- package/assets/modules/containerAppsEnv.bicep +43 -0
- package/assets/modules/controlGalaxyApp.bicep +129 -0
- package/assets/modules/entraClientSecret.bicep +31 -0
- package/assets/modules/innovationJob.bicep +91 -0
- package/assets/modules/insights.bicep +29 -0
- package/assets/modules/keyVault.bicep +42 -0
- package/assets/modules/platformIdentity.bicep +69 -0
- package/assets/modules/postgres.bicep +116 -0
- package/assets/modules/registry.bicep +38 -0
- package/assets/modules/storage.bicep +30 -0
- package/bin/setup.mjs +7 -0
- package/dist/azure/bicep.d.ts +9 -0
- package/dist/azure/bicep.d.ts.map +1 -0
- package/dist/azure/bicep.js +57 -0
- package/dist/azure/bicep.js.map +1 -0
- package/dist/azure/entra.d.ts +7 -0
- package/dist/azure/entra.d.ts.map +1 -0
- package/dist/azure/entra.js +45 -0
- package/dist/azure/entra.js.map +1 -0
- package/dist/azure/github.d.ts +5 -0
- package/dist/azure/github.d.ts.map +1 -0
- package/dist/azure/github.js +28 -0
- package/dist/azure/github.js.map +1 -0
- package/dist/azure/images.d.ts +15 -0
- package/dist/azure/images.d.ts.map +1 -0
- package/dist/azure/images.js +53 -0
- package/dist/azure/images.js.map +1 -0
- package/dist/azure/prerequisites.d.ts +8 -0
- package/dist/azure/prerequisites.d.ts.map +1 -0
- package/dist/azure/prerequisites.js +56 -0
- package/dist/azure/prerequisites.js.map +1 -0
- package/dist/azure/prompts.d.ts +10 -0
- package/dist/azure/prompts.d.ts.map +1 -0
- package/dist/azure/prompts.js +88 -0
- package/dist/azure/prompts.js.map +1 -0
- package/dist/azure/resource-group.d.ts +2 -0
- package/dist/azure/resource-group.d.ts.map +1 -0
- package/dist/azure/resource-group.js +8 -0
- package/dist/azure/resource-group.js.map +1 -0
- package/dist/azure/run.d.ts +2 -0
- package/dist/azure/run.d.ts.map +1 -0
- package/dist/azure/run.js +87 -0
- package/dist/azure/run.js.map +1 -0
- package/dist/azure/summary.d.ts +3 -0
- package/dist/azure/summary.d.ts.map +1 -0
- package/dist/azure/summary.js +13 -0
- package/dist/azure/summary.js.map +1 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +21 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/local/compose.d.ts +5 -0
- package/dist/local/compose.d.ts.map +1 -0
- package/dist/local/compose.js +40 -0
- package/dist/local/compose.js.map +1 -0
- package/dist/local/prerequisites.d.ts +3 -0
- package/dist/local/prerequisites.d.ts.map +1 -0
- package/dist/local/prerequisites.js +60 -0
- package/dist/local/prerequisites.js.map +1 -0
- package/dist/local/prompts.d.ts +6 -0
- package/dist/local/prompts.d.ts.map +1 -0
- package/dist/local/prompts.js +23 -0
- package/dist/local/prompts.js.map +1 -0
- package/dist/local/run.d.ts +2 -0
- package/dist/local/run.d.ts.map +1 -0
- package/dist/local/run.js +50 -0
- package/dist/local/run.js.map +1 -0
- package/dist/local/summary.d.ts +2 -0
- package/dist/local/summary.d.ts.map +1 -0
- package/dist/local/summary.js +16 -0
- package/dist/local/summary.js.map +1 -0
- package/dist/logo.d.ts +2 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +8 -0
- package/dist/logo.js.map +1 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +16 -0
- package/dist/ui.js.map +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +45 -0
- package/dist/utils.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The shared Container Apps managed environment. The backend, the control
|
|
2
|
+
// galaxy, and the innovation job all run inside this one environment so they
|
|
3
|
+
// share a log destination and can address each other over the internal
|
|
4
|
+
// environment domain when wired that way.
|
|
5
|
+
|
|
6
|
+
@description('Short, kebab-case portal name.')
|
|
7
|
+
param portalName string
|
|
8
|
+
|
|
9
|
+
@description('Azure region.')
|
|
10
|
+
param location string
|
|
11
|
+
|
|
12
|
+
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
13
|
+
name: '${portalName}-cae-logs'
|
|
14
|
+
location: location
|
|
15
|
+
properties: {
|
|
16
|
+
sku: { name: 'PerGB2018' }
|
|
17
|
+
retentionInDays: 30
|
|
18
|
+
workspaceCapping: { dailyQuotaGb: 2 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
|
|
23
|
+
name: '${portalName}-cae'
|
|
24
|
+
location: location
|
|
25
|
+
properties: {
|
|
26
|
+
appLogsConfiguration: {
|
|
27
|
+
destination: 'log-analytics'
|
|
28
|
+
logAnalyticsConfiguration: {
|
|
29
|
+
customerId: logAnalytics.properties.customerId
|
|
30
|
+
sharedKey: logAnalytics.listKeys().primarySharedKey
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
workloadProfiles: [
|
|
34
|
+
{
|
|
35
|
+
name: 'Consumption'
|
|
36
|
+
workloadProfileType: 'Consumption'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
output environmentId string = environment.id
|
|
43
|
+
output defaultDomain string = environment.properties.defaultDomain
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Control galaxy (admin portal) as a Container App. It serves the admin SPA
|
|
2
|
+
// and runs the BFF proxy that forwards the operator's Entra token to the
|
|
3
|
+
// backend control-plane API (shared-audience passthrough). It keeps its own
|
|
4
|
+
// portal-local database for the galaxy-local domains (connectors, branding,
|
|
5
|
+
// onboarding, users, settings); everything platform/AI-governance-related is
|
|
6
|
+
// owned by the backend and reached over BACKEND_BASE_URL.
|
|
7
|
+
//
|
|
8
|
+
// External ingress: operators reach the admin UI directly. The admin SPA
|
|
9
|
+
// acquires a token for the shared admin audience and the BFF forwards it to
|
|
10
|
+
// the backend; both validate the same audience.
|
|
11
|
+
|
|
12
|
+
@description('Short, kebab-case portal name.')
|
|
13
|
+
param portalName string
|
|
14
|
+
|
|
15
|
+
@description('Azure region.')
|
|
16
|
+
param location string
|
|
17
|
+
|
|
18
|
+
@description('Container Apps managed environment id (containerAppsEnv.bicep).')
|
|
19
|
+
param environmentId string
|
|
20
|
+
|
|
21
|
+
@description('User-assigned managed identity resource id (platformIdentity.bicep).')
|
|
22
|
+
param identityId string
|
|
23
|
+
|
|
24
|
+
@description('ACR login server (e.g. acmeacr.azurecr.io).')
|
|
25
|
+
param acrLoginServer string
|
|
26
|
+
|
|
27
|
+
@description('Fully qualified control-galaxy image reference, including tag.')
|
|
28
|
+
param controlGalaxyImage string
|
|
29
|
+
|
|
30
|
+
@description('Backend base URL the BFF forwards admin/config calls to (https://<backend fqdn>).')
|
|
31
|
+
param backendBaseUrl string
|
|
32
|
+
|
|
33
|
+
@description('Key Vault URI (https://<vault>.vault.azure.net/).')
|
|
34
|
+
param keyVaultUri string
|
|
35
|
+
|
|
36
|
+
@description('Key Vault secret name holding the control-galaxy database connection string.')
|
|
37
|
+
param databaseSecretName string
|
|
38
|
+
|
|
39
|
+
@description('Key Vault secret name holding the audit HMAC signing key.')
|
|
40
|
+
param auditSecretName string
|
|
41
|
+
|
|
42
|
+
@description('Key Vault secret name holding the Entra app-registration client secret (connector OBO).')
|
|
43
|
+
param entraClientSecretName string
|
|
44
|
+
|
|
45
|
+
@description('Application Insights connection string.')
|
|
46
|
+
param insightsConnectionString string
|
|
47
|
+
|
|
48
|
+
@description('Entra tenant id for token validation.')
|
|
49
|
+
param entraTenantId string
|
|
50
|
+
|
|
51
|
+
@description('Admin/config-plane Entra app registration client id (shared audience with the backend).')
|
|
52
|
+
param entraAdminClientId string
|
|
53
|
+
|
|
54
|
+
@description('Portal organization DID.')
|
|
55
|
+
param portalOrgDid string
|
|
56
|
+
|
|
57
|
+
@description('Portal organization display name.')
|
|
58
|
+
param portalOrgName string
|
|
59
|
+
|
|
60
|
+
@description('CPU cores.')
|
|
61
|
+
param cpuCores string = '0.5'
|
|
62
|
+
|
|
63
|
+
@description('Memory.')
|
|
64
|
+
param memorySize string = '1Gi'
|
|
65
|
+
|
|
66
|
+
// Domain-specific secrets[] and env[]. These literal arrays stay here (rather
|
|
67
|
+
// than in containerApp.bicep) so the infra contract test can parse the env
|
|
68
|
+
// contract from this module. The shared containerApps scaffolding lives in
|
|
69
|
+
// containerApp.bicep.
|
|
70
|
+
var controlGalaxySecrets = [
|
|
71
|
+
{
|
|
72
|
+
name: 'database-url'
|
|
73
|
+
keyVaultUrl: '${keyVaultUri}secrets/${databaseSecretName}'
|
|
74
|
+
identity: identityId
|
|
75
|
+
}
|
|
76
|
+
{
|
|
77
|
+
name: 'audit-signing-key'
|
|
78
|
+
keyVaultUrl: '${keyVaultUri}secrets/${auditSecretName}'
|
|
79
|
+
identity: identityId
|
|
80
|
+
}
|
|
81
|
+
{
|
|
82
|
+
name: 'entra-client-secret'
|
|
83
|
+
keyVaultUrl: '${keyVaultUri}secrets/${entraClientSecretName}'
|
|
84
|
+
identity: identityId
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
var controlGalaxyEnv = [
|
|
89
|
+
{ name: 'NODE_ENV', value: 'production' }
|
|
90
|
+
{ name: 'HOST', value: '0.0.0.0' }
|
|
91
|
+
{ name: 'PORT', value: '5174' }
|
|
92
|
+
{ name: 'BACKEND_BASE_URL', value: backendBaseUrl }
|
|
93
|
+
{ name: 'DATABASE_URL', secretRef: 'database-url' }
|
|
94
|
+
{ name: 'AUDIT_SIGNING_KEY', secretRef: 'audit-signing-key' }
|
|
95
|
+
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: insightsConnectionString }
|
|
96
|
+
{ name: 'KEY_VAULT_URI', value: keyVaultUri }
|
|
97
|
+
// Foundry OBO (server/agents/external/foundry-deps.ts) reads
|
|
98
|
+
// AZURE_KEY_VAULT_URL; other code reads KEY_VAULT_URI. Keep both.
|
|
99
|
+
{ name: 'AZURE_KEY_VAULT_URL', value: keyVaultUri }
|
|
100
|
+
{ name: 'ENTRA_TENANT_ID', value: entraTenantId }
|
|
101
|
+
{ name: 'ENTRA_CLIENT_ID', value: entraAdminClientId }
|
|
102
|
+
// Connector OBO (server/index.ts) reads ENTRA_CLIENT_SECRET.
|
|
103
|
+
{ name: 'ENTRA_CLIENT_SECRET', secretRef: 'entra-client-secret' }
|
|
104
|
+
{ name: 'BACKEND_API_AUDIENCE', value: 'api://${entraAdminClientId}' }
|
|
105
|
+
{ name: 'PORTAL_ORG_DID', value: portalOrgDid }
|
|
106
|
+
{ name: 'PORTAL_ORG_NAME', value: portalOrgName }
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
module controlGalaxy 'containerApp.bicep' = {
|
|
110
|
+
name: 'controlGalaxyContainerApp'
|
|
111
|
+
params: {
|
|
112
|
+
portalName: portalName
|
|
113
|
+
appSuffix: 'control-galaxy'
|
|
114
|
+
location: location
|
|
115
|
+
environmentId: environmentId
|
|
116
|
+
identityId: identityId
|
|
117
|
+
acrLoginServer: acrLoginServer
|
|
118
|
+
image: controlGalaxyImage
|
|
119
|
+
port: 5174
|
|
120
|
+
maxReplicas: 2
|
|
121
|
+
cpuCores: cpuCores
|
|
122
|
+
memorySize: memorySize
|
|
123
|
+
secrets: controlGalaxySecrets
|
|
124
|
+
env: controlGalaxyEnv
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
output appName string = controlGalaxy.outputs.appName
|
|
129
|
+
output fqdn string = controlGalaxy.outputs.fqdn
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Entra app-registration client secret for connector OBO (on-behalf-of) flows.
|
|
2
|
+
// apps/control-galaxy/server/index.ts reads process.env.ENTRA_CLIENT_SECRET to
|
|
3
|
+
// exchange the operator token for a connector access token. Without it, every
|
|
4
|
+
// enabled connector fails at its first OBO call. Written here (not in
|
|
5
|
+
// keyVault.bicep) so the @secure() value stays inside a module that references
|
|
6
|
+
// the vault by name param, keeping it out of the orchestration deployment graph
|
|
7
|
+
// as an output. Mirrors auditSecret.bicep.
|
|
8
|
+
|
|
9
|
+
@description('Name of the existing Key Vault to write the secret into.')
|
|
10
|
+
param keyVaultName string
|
|
11
|
+
|
|
12
|
+
@secure()
|
|
13
|
+
@description('Entra app-registration client secret value.')
|
|
14
|
+
param entraClientSecret string
|
|
15
|
+
|
|
16
|
+
@description('Secret name.')
|
|
17
|
+
param secretName string = 'entra-client-secret'
|
|
18
|
+
|
|
19
|
+
resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
20
|
+
name: keyVaultName
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resource secret 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
|
|
24
|
+
parent: vault
|
|
25
|
+
name: secretName
|
|
26
|
+
properties: {
|
|
27
|
+
value: entraClientSecret
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
output secretName string = secret.name
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The Innovation Implementer as a Container Apps Job, on the SHARED
|
|
2
|
+
// environment and SHARED platform identity (no longer its own env/logs/identity).
|
|
3
|
+
//
|
|
4
|
+
// When an admin approves an innovation proposal, the control galaxy triggers
|
|
5
|
+
// this short-lived job. The agent reads the proposal, generates code following
|
|
6
|
+
// the galaxy's CLAUDE.md + brain/wisdom/ conventions, opens a PR, then dies.
|
|
7
|
+
// Jobs scale to zero with per-second billing; each run is a fresh container so
|
|
8
|
+
// state cannot leak between proposals.
|
|
9
|
+
//
|
|
10
|
+
// Credentials (Anthropic key, GitHub PAT, callback HMAC) are read from Key
|
|
11
|
+
// Vault at job start over the shared user-assigned identity. No long-lived
|
|
12
|
+
// secret is baked into the image.
|
|
13
|
+
|
|
14
|
+
@description('Short name prefix (matches main.bicep).')
|
|
15
|
+
param portalName string
|
|
16
|
+
|
|
17
|
+
@description('Azure region.')
|
|
18
|
+
param location string
|
|
19
|
+
|
|
20
|
+
@description('Shared Container Apps managed environment id.')
|
|
21
|
+
param environmentId string
|
|
22
|
+
|
|
23
|
+
@description('Shared user-assigned managed identity resource id (already granted Key Vault Secrets User + AcrPull).')
|
|
24
|
+
param identityId string
|
|
25
|
+
|
|
26
|
+
@description('Identity client id, surfaced to the container so it can authenticate to Azure as the managed identity.')
|
|
27
|
+
param identityClientId string
|
|
28
|
+
|
|
29
|
+
@description('Key Vault name the job reads its run secrets from.')
|
|
30
|
+
param keyVaultName string
|
|
31
|
+
|
|
32
|
+
@description('Container image for the implementer agent.')
|
|
33
|
+
param implementerImage string = 'ghcr.io/fortytwoservices/ai-universe-innovation-implementer:v1'
|
|
34
|
+
|
|
35
|
+
@description('CPU cores. Decimal allowed.')
|
|
36
|
+
param cpuCores string = '1.0'
|
|
37
|
+
|
|
38
|
+
@description('Memory.')
|
|
39
|
+
param memorySize string = '2Gi'
|
|
40
|
+
|
|
41
|
+
@description('Replica timeout in seconds. Hard upper bound on agent runtime.')
|
|
42
|
+
param replicaTimeoutSeconds int = 1800
|
|
43
|
+
|
|
44
|
+
resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
45
|
+
name: keyVaultName
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resource implementerJob 'Microsoft.App/jobs@2024-03-01' = {
|
|
49
|
+
name: '${portalName}-innovation-implementer'
|
|
50
|
+
location: location
|
|
51
|
+
identity: {
|
|
52
|
+
type: 'UserAssigned'
|
|
53
|
+
userAssignedIdentities: {
|
|
54
|
+
'${identityId}': {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
properties: {
|
|
58
|
+
environmentId: environmentId
|
|
59
|
+
workloadProfileName: 'Consumption'
|
|
60
|
+
configuration: {
|
|
61
|
+
triggerType: 'Manual'
|
|
62
|
+
replicaTimeout: replicaTimeoutSeconds
|
|
63
|
+
replicaRetryLimit: 1
|
|
64
|
+
manualTriggerConfig: {
|
|
65
|
+
replicaCompletionCount: 1
|
|
66
|
+
parallelism: 1
|
|
67
|
+
}
|
|
68
|
+
secrets: []
|
|
69
|
+
}
|
|
70
|
+
template: {
|
|
71
|
+
containers: [
|
|
72
|
+
{
|
|
73
|
+
name: 'implementer'
|
|
74
|
+
image: implementerImage
|
|
75
|
+
resources: {
|
|
76
|
+
cpu: json(cpuCores)
|
|
77
|
+
memory: memorySize
|
|
78
|
+
}
|
|
79
|
+
env: [
|
|
80
|
+
{ name: 'KEY_VAULT_NAME', value: keyVault.name }
|
|
81
|
+
{ name: 'KEY_VAULT_URI', value: keyVault.properties.vaultUri }
|
|
82
|
+
{ name: 'PORTAL_NAME', value: portalName }
|
|
83
|
+
{ name: 'AZURE_CLIENT_ID', value: identityClientId }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
output jobName string = implementerJob.name
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Application Insights resource (Log Analytics workspace-backed, the modern variant).
|
|
2
|
+
|
|
3
|
+
@description('Short name prefix for the portal.')
|
|
4
|
+
param portalName string
|
|
5
|
+
|
|
6
|
+
@description('Azure region.')
|
|
7
|
+
param location string
|
|
8
|
+
|
|
9
|
+
resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
10
|
+
name: '${portalName}-logs'
|
|
11
|
+
location: location
|
|
12
|
+
properties: {
|
|
13
|
+
sku: { name: 'PerGB2018' }
|
|
14
|
+
retentionInDays: 30
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
|
19
|
+
name: '${portalName}-insights'
|
|
20
|
+
location: location
|
|
21
|
+
kind: 'web'
|
|
22
|
+
properties: {
|
|
23
|
+
Application_Type: 'web'
|
|
24
|
+
WorkspaceResourceId: logWorkspace.id
|
|
25
|
+
IngestionMode: 'LogAnalytics'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
output connectionString string = appInsights.properties.ConnectionString
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Key Vault — owned and provisioned by this module. Domain-specific
|
|
2
|
+
// secret writes (postgres connection strings, audit HMAC signing key,
|
|
3
|
+
// app-reg secrets) live in the modules that own those domains and
|
|
4
|
+
// reference this vault by name, so secret values never travel through
|
|
5
|
+
// this orchestration layer as outputs. RBAC-only authorization model;
|
|
6
|
+
// the shared platform managed identity gets `Key Vault Secrets User`
|
|
7
|
+
// in platformIdentity.bicep.
|
|
8
|
+
|
|
9
|
+
@description('Short name prefix for the portal.')
|
|
10
|
+
param portalName string
|
|
11
|
+
|
|
12
|
+
@description('Azure region.')
|
|
13
|
+
param location string
|
|
14
|
+
|
|
15
|
+
@description('Entra ID tenant for RBAC.')
|
|
16
|
+
param tenantId string
|
|
17
|
+
|
|
18
|
+
// Collision-resistant, deterministic vault name. A plain take('${portalName}-kv', 24)
|
|
19
|
+
// drops the '-kv' suffix for long portalNames (~22 chars) and can collide across
|
|
20
|
+
// resource groups. Appending a stable uniqueString(resourceGroup().id) segment
|
|
21
|
+
// keeps the name unique and reconstructible. uniqueString is deterministic per RG,
|
|
22
|
+
// so app.bicep rebuilds the SAME name as an existing reference. Keep both name
|
|
23
|
+
// expressions (here and in app.bicep) in lock-step. Max length 24:
|
|
24
|
+
// take(portalName, 9) + '-kv-' (4) + take(unique, 6) = up to 19 chars.
|
|
25
|
+
var vaultName = '${take(portalName, 9)}-kv-${take(uniqueString(resourceGroup().id), 6)}'
|
|
26
|
+
|
|
27
|
+
resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
|
|
28
|
+
name: vaultName
|
|
29
|
+
location: location
|
|
30
|
+
properties: {
|
|
31
|
+
tenantId: tenantId
|
|
32
|
+
sku: { family: 'A', name: 'standard' }
|
|
33
|
+
enableRbacAuthorization: true
|
|
34
|
+
enableSoftDelete: true
|
|
35
|
+
softDeleteRetentionInDays: 7
|
|
36
|
+
enablePurgeProtection: true
|
|
37
|
+
publicNetworkAccess: 'Enabled'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
output vaultName string = vault.name
|
|
42
|
+
output vaultUri string = vault.properties.vaultUri
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// One user-assigned managed identity shared by the platform Container Apps
|
|
2
|
+
// (backend, control galaxy) and the innovation job. Using a user-assigned
|
|
3
|
+
// identity avoids the chicken-and-egg of system-assigned identities needing
|
|
4
|
+
// AcrPull before the first image pull: the identity and its role assignments
|
|
5
|
+
// exist before any app references it.
|
|
6
|
+
//
|
|
7
|
+
// Grants:
|
|
8
|
+
// - AcrPull on the customer registry (pull images with no registry secret)
|
|
9
|
+
// - Key Vault Secrets Officer on the vault. Read is needed to resolve
|
|
10
|
+
// DATABASE_URL, HMAC keys, and app-reg secrets as Container Apps secret
|
|
11
|
+
// references. WRITE is needed because the control galaxy persists provider
|
|
12
|
+
// secrets at runtime: apps/control-galaxy/server/config/router.ts calls
|
|
13
|
+
// client.setSecret(...). Secrets User is read-only and would 403 on that
|
|
14
|
+
// write, silently dropping provider secrets. Officer includes read access.
|
|
15
|
+
|
|
16
|
+
@description('Short, kebab-case portal name.')
|
|
17
|
+
param portalName string
|
|
18
|
+
|
|
19
|
+
@description('Azure region.')
|
|
20
|
+
param location string
|
|
21
|
+
|
|
22
|
+
@description('Registry name (from registry.bicep) to grant AcrPull on.')
|
|
23
|
+
param registryName string
|
|
24
|
+
|
|
25
|
+
@description('Key Vault name (from keyVault.bicep) to grant Secrets User on.')
|
|
26
|
+
param keyVaultName string
|
|
27
|
+
|
|
28
|
+
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
|
|
29
|
+
name: '${portalName}-platform-id'
|
|
30
|
+
location: location
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
|
|
34
|
+
name: registryName
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
38
|
+
name: keyVaultName
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Built-in role definition ids.
|
|
42
|
+
var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d'
|
|
43
|
+
// Key Vault Secrets Officer (read + write). Write is required so the control
|
|
44
|
+
// galaxy can persist provider secrets via setSecret(...) at runtime.
|
|
45
|
+
var keyVaultSecretsOfficerRoleId = 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'
|
|
46
|
+
|
|
47
|
+
resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
48
|
+
scope: registry
|
|
49
|
+
name: guid(registry.id, identity.id, acrPullRoleId)
|
|
50
|
+
properties: {
|
|
51
|
+
principalId: identity.properties.principalId
|
|
52
|
+
principalType: 'ServicePrincipal'
|
|
53
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleId)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resource kvSecretsOfficer 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
58
|
+
scope: keyVault
|
|
59
|
+
name: guid(keyVault.id, identity.id, keyVaultSecretsOfficerRoleId)
|
|
60
|
+
properties: {
|
|
61
|
+
principalId: identity.properties.principalId
|
|
62
|
+
principalType: 'ServicePrincipal'
|
|
63
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsOfficerRoleId)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
output identityId string = identity.id
|
|
68
|
+
output clientId string = identity.properties.clientId
|
|
69
|
+
output principalId string = identity.properties.principalId
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// PostgreSQL Flexible Server. One server hosts two databases on its own
|
|
2
|
+
// schema: `backend` (the headless kernel: providers, models, policies, agents,
|
|
3
|
+
// galaxy registry, audit, spend) and `control_galaxy` (the admin portal's
|
|
4
|
+
// galaxy-local domains: connectors, branding, onboarding, users, settings).
|
|
5
|
+
// v1 ships with public-access + Azure-services allowlist for simplicity.
|
|
6
|
+
// Production should switch to private endpoint.
|
|
7
|
+
//
|
|
8
|
+
// SECURITY: the admin password never crosses a module boundary as an output.
|
|
9
|
+
// The module writes each runtime connection string directly into the existing
|
|
10
|
+
// Key Vault, keeping the secret value inside @secure() context for its entire
|
|
11
|
+
// lifetime. Outputs expose only the FQDN and the secret names.
|
|
12
|
+
//
|
|
13
|
+
// AAD-only auth is the recommended next step (flexibleServers/administrators
|
|
14
|
+
// with the platform managed identity) and removes the password entirely.
|
|
15
|
+
|
|
16
|
+
@description('Short name prefix for the portal.')
|
|
17
|
+
param portalName string
|
|
18
|
+
|
|
19
|
+
@description('Azure region.')
|
|
20
|
+
param location string
|
|
21
|
+
|
|
22
|
+
@description('Administrator login name.')
|
|
23
|
+
param administratorLogin string
|
|
24
|
+
|
|
25
|
+
@secure()
|
|
26
|
+
@description('Administrator password. Written straight to Key Vault, never surfaced as a module output.')
|
|
27
|
+
param administratorPassword string
|
|
28
|
+
|
|
29
|
+
@description('Compute SKU.')
|
|
30
|
+
param sku string
|
|
31
|
+
|
|
32
|
+
@description('Storage in GB.')
|
|
33
|
+
param storageGb int
|
|
34
|
+
|
|
35
|
+
@description('Postgres major version.')
|
|
36
|
+
param version string = '16'
|
|
37
|
+
|
|
38
|
+
@description('Name of the existing Key Vault to write the connection-string secrets into. Must be in the same resource group.')
|
|
39
|
+
param keyVaultName string
|
|
40
|
+
|
|
41
|
+
// Each entry becomes a database on the server and a connection-string secret
|
|
42
|
+
// in the vault. Backend and control galaxy keep separate schemas.
|
|
43
|
+
var databases = [
|
|
44
|
+
{ name: 'backend', secret: 'postgres-connection-string-backend' }
|
|
45
|
+
{ name: 'control_galaxy', secret: 'postgres-connection-string-control' }
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
var serverName = '${portalName}-pg'
|
|
49
|
+
|
|
50
|
+
resource server 'Microsoft.DBforPostgreSQL/flexibleServers@2024-03-01-preview' = {
|
|
51
|
+
name: serverName
|
|
52
|
+
location: location
|
|
53
|
+
sku: {
|
|
54
|
+
name: sku
|
|
55
|
+
tier: startsWith(sku, 'Standard_B') ? 'Burstable' : 'GeneralPurpose'
|
|
56
|
+
}
|
|
57
|
+
properties: {
|
|
58
|
+
version: version
|
|
59
|
+
administratorLogin: administratorLogin
|
|
60
|
+
administratorLoginPassword: administratorPassword
|
|
61
|
+
storage: {
|
|
62
|
+
storageSizeGB: storageGb
|
|
63
|
+
autoGrow: 'Enabled'
|
|
64
|
+
}
|
|
65
|
+
backup: {
|
|
66
|
+
backupRetentionDays: 7
|
|
67
|
+
geoRedundantBackup: 'Disabled'
|
|
68
|
+
}
|
|
69
|
+
highAvailability: { mode: 'Disabled' }
|
|
70
|
+
network: {
|
|
71
|
+
publicNetworkAccess: 'Enabled'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resource allowAzureServices 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2024-03-01-preview' = {
|
|
77
|
+
parent: server
|
|
78
|
+
name: 'AllowAllAzureIps'
|
|
79
|
+
properties: {
|
|
80
|
+
startIpAddress: '0.0.0.0'
|
|
81
|
+
endIpAddress: '0.0.0.0'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resource pgDatabases 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2024-03-01-preview' = [
|
|
86
|
+
for db in databases: {
|
|
87
|
+
parent: server
|
|
88
|
+
name: db.name
|
|
89
|
+
properties: {
|
|
90
|
+
charset: 'UTF8'
|
|
91
|
+
collation: 'en_US.utf8'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
// Existing-vault reference + secret write. The vault is provisioned by
|
|
97
|
+
// modules/keyVault.bicep before this module runs (main.bicep wires the order).
|
|
98
|
+
// The secret values are interpolated INSIDE this module's @secure() scope,
|
|
99
|
+
// never crossing into an output.
|
|
100
|
+
resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
101
|
+
name: keyVaultName
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
resource connectionStringSecrets 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = [
|
|
105
|
+
for db in databases: {
|
|
106
|
+
parent: vault
|
|
107
|
+
name: db.secret
|
|
108
|
+
properties: {
|
|
109
|
+
value: 'postgresql://${administratorLogin}:${administratorPassword}@${server.properties.fullyQualifiedDomainName}:5432/${db.name}?sslmode=require'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
output fqdn string = server.properties.fullyQualifiedDomainName
|
|
115
|
+
output backendConnectionSecretName string = databases[0].secret
|
|
116
|
+
output controlConnectionSecretName string = databases[1].secret
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Azure Container Registry — the per-customer registry the platform images
|
|
2
|
+
// are pulled from at runtime. Fortytwo CI builds and pushes the backend (and
|
|
3
|
+
// control-galaxy) images to a central Fortytwo ACR; at deploy time those
|
|
4
|
+
// images are imported into this customer-owned registry (`az acr import`) so
|
|
5
|
+
// the Container Apps pull them over a managed identity with no stored secret.
|
|
6
|
+
//
|
|
7
|
+
// Admin user is disabled on purpose: pulls are identity-based (AcrPull granted
|
|
8
|
+
// in platformIdentity.bicep), never username/password.
|
|
9
|
+
|
|
10
|
+
@description('Short, kebab-case portal name. Used as a prefix; ACR names are alphanumeric only so the prefix is sanitized.')
|
|
11
|
+
@minLength(2)
|
|
12
|
+
param portalName string
|
|
13
|
+
|
|
14
|
+
@description('Azure region.')
|
|
15
|
+
param location string
|
|
16
|
+
|
|
17
|
+
@description('Registry SKU. Standard is the floor for production (webhooks, larger storage); Premium adds geo-replication + private link.')
|
|
18
|
+
@allowed([
|
|
19
|
+
'Standard'
|
|
20
|
+
'Premium'
|
|
21
|
+
])
|
|
22
|
+
param sku string = 'Standard'
|
|
23
|
+
|
|
24
|
+
// ACR names: 5-50 chars, alphanumeric only, globally unique. Strip dashes.
|
|
25
|
+
var registryName = take('${replace(portalName, '-', '')}acr', 50)
|
|
26
|
+
|
|
27
|
+
resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
|
|
28
|
+
name: registryName
|
|
29
|
+
location: location
|
|
30
|
+
sku: { name: sku }
|
|
31
|
+
properties: {
|
|
32
|
+
adminUserEnabled: false
|
|
33
|
+
publicNetworkAccess: 'Enabled'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
output registryName string = registry.name
|
|
38
|
+
output loginServer string = registry.properties.loginServer
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Storage account for future blob uploads (attachments, exports, audit-log archives).
|
|
2
|
+
|
|
3
|
+
@description('Short name prefix for the portal.')
|
|
4
|
+
param portalName string
|
|
5
|
+
|
|
6
|
+
@description('Azure region.')
|
|
7
|
+
param location string
|
|
8
|
+
|
|
9
|
+
// Collision-resistant, deterministic storage account name. The old
|
|
10
|
+
// take(...'${portalName}storage'..., 24) truncated the 'storage' suffix for long
|
|
11
|
+
// portalNames and could collide (account names are globally unique). Storage
|
|
12
|
+
// account names are 3-24 chars, lowercase alphanumeric only; uniqueString already
|
|
13
|
+
// returns lowercase alphanumeric. take(clean, 11) + 'st' (2) + take(unique, 11) = up to 24.
|
|
14
|
+
var portalNameClean = toLower(replace(portalName, '-', ''))
|
|
15
|
+
var accountName = '${take(portalNameClean, 11)}st${take(uniqueString(resourceGroup().id), 11)}'
|
|
16
|
+
|
|
17
|
+
resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
|
|
18
|
+
name: accountName
|
|
19
|
+
location: location
|
|
20
|
+
sku: { name: 'Standard_LRS' }
|
|
21
|
+
kind: 'StorageV2'
|
|
22
|
+
properties: {
|
|
23
|
+
minimumTlsVersion: 'TLS1_2'
|
|
24
|
+
supportsHttpsTrafficOnly: true
|
|
25
|
+
allowBlobPublicAccess: false
|
|
26
|
+
defaultToOAuthAuthentication: true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
output accountName string = storage.name
|
package/bin/setup.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EntraConfig } from './entra.js';
|
|
2
|
+
import type { AzureConfig } from './prompts.js';
|
|
3
|
+
export type BicepOutputs = {
|
|
4
|
+
backendUrl: string;
|
|
5
|
+
controlGalaxyUrl: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseBicepOutputs(raw: string): BicepOutputs;
|
|
8
|
+
export declare function deployBicep(config: AzureConfig, entra: EntraConfig, githubInstallationId: string): Promise<BicepOutputs>;
|
|
9
|
+
//# sourceMappingURL=bicep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bicep.d.ts","sourceRoot":"","sources":["../../src/azure/bicep.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAWF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAc3D;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
|