diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8ed2efd..56c8594 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,22 +7,31 @@ jobs: deploy: name: Deploy runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v5 - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 - name: Setup Node uses: actions/setup-node@v5 with: node-version: 24 + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v2 + with: + install_components: gke-gcloud-auth-plugin - name: Install Packages run: npm install working-directory: ./pulumi + - name: Authenticate Pulumi + uses: pulumi/auth-actions@v1 + with: + organization: meiermade + requested-token-type: urn:pulumi:token-type:access_token:personal + scope: user:meiermade - name: Deploy uses: pulumi/actions@v6 with: work-dir: ./pulumi command: up stack-name: prod - env: - PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 3f24321..5407c22 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -30,6 +30,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + id-token: write pull-requests: write needs: - test @@ -39,9 +40,19 @@ jobs: uses: actions/setup-node@v5 with: node-version: 24 + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@v2 + with: + install_components: gke-gcloud-auth-plugin - name: Install Packages run: npm install working-directory: ./pulumi + - name: Authenticate Pulumi + uses: pulumi/auth-actions@v1 + with: + organization: meiermade + requested-token-type: urn:pulumi:token-type:access_token:personal + scope: user:meiermade - name: Preview uses: pulumi/actions@v6 with: @@ -50,5 +61,3 @@ jobs: stack-name: prod diff: true comment-on-pr: true - env: - PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bde4509..cbb79f9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,6 +9,9 @@ jobs: publish: name: Publish runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v5 - name: Setup dotnet @@ -24,8 +27,12 @@ jobs: - name: Install Packages run: dotnet paket install working-directory: ./sln + - name: Authenticate Pulumi + uses: pulumi/auth-actions@v1 + with: + organization: meiermade + requested-token-type: urn:pulumi:token-type:access_token:personal + scope: user:meiermade - name: Publish - run: ./fake.sh PushNugets + run: pulumi env run fsharpviewengine/nuget -- ./fake.sh PushNugets working-directory: ./sln - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/pulumi/index.ts b/pulumi/index.ts index 22e4135..ad6bdc9 100644 --- a/pulumi/index.ts +++ b/pulumi/index.ts @@ -1,4 +1,3 @@ -import './src/aws' import './src/cloudflare' import './src/docker' import './src/k8s' diff --git a/pulumi/package-lock.json b/pulumi/package-lock.json index 90cdb40..2260f7c 100644 --- a/pulumi/package-lock.json +++ b/pulumi/package-lock.json @@ -6,7 +6,6 @@ "": { "name": "fsharp-view-engine", "dependencies": { - "@pulumi/aws": "^7.16.0", "@pulumi/cloudflare": "^6.10.0", "@pulumi/docker-build": "^0.0.15", "@pulumi/kubernetes": "^4.18", @@ -490,7 +489,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -787,16 +785,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@pulumi/aws": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@pulumi/aws/-/aws-7.16.0.tgz", - "integrity": "sha512-taLHQH83bsZ37sifIc0xUA7SgX6xjsuJgkLGQ+Y7NMriNMCnk+9v801bs4r+YDy480zFF/yi195RuX77ZDXHPg==", - "license": "Apache-2.0", - "dependencies": { - "@pulumi/pulumi": "^3.142.0", - "mime": "^2.0.0" - } - }, "node_modules/@pulumi/cloudflare": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/@pulumi/cloudflare/-/cloudflare-6.13.0.tgz", @@ -1103,7 +1091,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2141,18 +2128,6 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -2759,7 +2734,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/pulumi/package.json b/pulumi/package.json index 271dca8..3a03b36 100644 --- a/pulumi/package.json +++ b/pulumi/package.json @@ -5,7 +5,6 @@ "@types/node": "^24.10.9" }, "dependencies": { - "@pulumi/aws": "^7.16.0", "@pulumi/cloudflare": "^6.10.0", "@pulumi/docker-build": "^0.0.15", "@pulumi/kubernetes": "^4.18", diff --git a/pulumi/src/aws/index.ts b/pulumi/src/aws/index.ts deleted file mode 100644 index 982aaed..0000000 --- a/pulumi/src/aws/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './provider' -import './repository' diff --git a/pulumi/src/aws/provider.ts b/pulumi/src/aws/provider.ts deleted file mode 100644 index 5ef1b83..0000000 --- a/pulumi/src/aws/provider.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as aws from '@pulumi/aws' -import * as config from '../config' - -export const provider = new aws.Provider('default', { - region: config.awsConfig.region, - allowedAccountIds: [ config.awsConfig.accountId ] -}) diff --git a/pulumi/src/aws/repository.ts b/pulumi/src/aws/repository.ts deleted file mode 100644 index c40f473..0000000 --- a/pulumi/src/aws/repository.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as aws from '@pulumi/aws' -import { provider } from './provider' -import * as config from '../config' - -export const repo = new aws.ecr.Repository(config.identifier, { - name: config.identifier -}, { provider }) - -export const credentials = aws.ecr.getAuthorizationTokenOutput({ - registryId: repo.registryId -}, { provider }) - -new aws.ecr.RepositoryPolicy(config.identifier, { - repository: repo.name, - policy: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - AWS: config.awsConfig.eksNodeManagerArn - }, - Action: [ - 'ecr:GetDownloadUrlForLayer', - 'ecr:BatchGetImage', - 'ecr:BatchCheckLayerAvailability' - ] - } - ] - } -}, { provider: provider }) - -new aws.ecr.LifecyclePolicy(config.identifier, { - repository: repo.name, - policy: { - rules: [{ - rulePriority: 1, - selection: { - tagStatus: 'any', - countType: 'imageCountMoreThan', - countNumber: 1 - }, - action: { - type: 'expire' - }, - }] - } -}, { provider }) diff --git a/pulumi/src/config.ts b/pulumi/src/config.ts index 51a4ba2..918feb0 100644 --- a/pulumi/src/config.ts +++ b/pulumi/src/config.ts @@ -5,16 +5,15 @@ export const rootDir = path.dirname(path.dirname(__dirname)) export const identifier = 'fsharpviewengine' -const rawAwsConfig = new pulumi.Config('aws') -const rawCloudflareConfig = new pulumi.Config('cloudflare') -const rawK8sConfig = new pulumi.Config('k8s') +const rawDockerConfig = new pulumi.Config('docker') -export const awsConfig = { - accountId: rawAwsConfig.require('platformAccountId'), - region: rawAwsConfig.require('region'), - eksNodeManagerArn: rawAwsConfig.require('eksNodeManagerArn') +export const dockerConfig = { + registryUri: rawDockerConfig.require('registryUri'), + registryAccessToken: rawDockerConfig.requireSecret('registryAccessToken'), } +const rawCloudflareConfig = new pulumi.Config('cloudflare') + export const cloudflareConfig = { accountId: rawCloudflareConfig.require('accountId'), apiToken: rawCloudflareConfig.requireSecret('apiToken'), @@ -22,6 +21,8 @@ export const cloudflareConfig = { cloudflaredVersion: '2026.2.0' } +const rawK8sConfig = new pulumi.Config('k8s') + export const k8sConfig = { namespace: rawK8sConfig.require('namespace'), } diff --git a/pulumi/src/docker/image.ts b/pulumi/src/docker/image.ts index 7d952f7..29928e4 100644 --- a/pulumi/src/docker/image.ts +++ b/pulumi/src/docker/image.ts @@ -1,22 +1,28 @@ import * as pulumi from '@pulumi/pulumi' -import * as docker from '@pulumi/docker-build' +import * as dockerBuild from '@pulumi/docker-build' import * as path from 'path' import { provider } from './provider' -import { repo, credentials } from '../aws/repository' import * as config from '../config' -export const image = new docker.Image(config.identifier, { - tags: [pulumi.interpolate `${repo.repositoryUrl}:latest`], - push: true, +const registryUri = config.dockerConfig.registryUri +const registryHost = registryUri.split('/')[0] + +export const image = new dockerBuild.Image(config.identifier, { + tags: [ + pulumi.interpolate`${registryUri}/${config.identifier}` + ], context: { location: path.join(config.rootDir, 'sln'), }, - platforms: ['linux/arm64'], + platforms: [ + dockerBuild.Platform.Linux_amd64 + ], + push: true, registries: [{ - address: repo.repositoryUrl, - username: credentials.userName, - password: credentials.password - }] + address: registryHost, + username: 'oauth2accesstoken', + password: config.dockerConfig.registryAccessToken, + }], }, { provider }) export const imageRef = image.ref diff --git a/pulumi/src/docker/provider.ts b/pulumi/src/docker/provider.ts index 77f0cbb..c949240 100644 --- a/pulumi/src/docker/provider.ts +++ b/pulumi/src/docker/provider.ts @@ -1,3 +1,3 @@ -import * as docker from '@pulumi/docker-build' +import * as dockerBuild from '@pulumi/docker-build' -export const provider = new docker.Provider('default') +export const provider = new dockerBuild.Provider('default') diff --git a/pulumi/src/k8s/deployment.ts b/pulumi/src/k8s/deployment.ts index 7501b57..3d90787 100644 --- a/pulumi/src/k8s/deployment.ts +++ b/pulumi/src/k8s/deployment.ts @@ -17,16 +17,31 @@ let appConfigMap = new k8s.core.v1.ConfigMap(config.identifier, { const labels = { 'app.kubernetes.io/name': config.identifier } -const tunnelSecret = new k8s.core.v1.Secret(`${config.identifier}-cloudflared`, { +const cloudflaredSecret = new k8s.core.v1.Secret(`${config.identifier}-cloudflared`, { metadata: { name: `${config.identifier}-cloudflared`, namespace: config.k8sConfig.namespace }, stringData: { - TUNNEL_TOKEN: tunnel.tunnelToken + TUNNEL_TOKEN: tunnel.tunnelToken, + TUNNEL_METRICS: '0.0.0.0:2000' } }, { provider }) +const podSecurityContext: k8s.types.input.core.v1.PodSecurityContext = { + runAsNonRoot: true, + seccompProfile: { + type: 'RuntimeDefault' + } +} + +const containerSecurityContext: k8s.types.input.core.v1.SecurityContext = { + allowPrivilegeEscalation: false, + capabilities: { + drop: ['ALL'] + } +} + const deployment = new k8s.apps.v1.Deployment(config.identifier, { metadata: { name: config.identifier, @@ -38,10 +53,12 @@ const deployment = new k8s.apps.v1.Deployment(config.identifier, { template: { metadata: { labels: labels }, spec: { + securityContext: podSecurityContext, containers: [ { name: config.identifier, image: image.imageRef, + securityContext: containerSecurityContext, imagePullPolicy: 'IfNotPresent', envFrom: [ { configMapRef: { name: appConfigMap.metadata.name } } ], livenessProbe: { @@ -62,44 +79,25 @@ const deployment = new k8s.apps.v1.Deployment(config.identifier, { { name: 'cloudflared', image: `cloudflare/cloudflared:${config.cloudflareConfig.cloudflaredVersion}`, + securityContext: containerSecurityContext, args: [ 'tunnel', '--no-autoupdate', - '--metrics', '0.0.0.0:2000', 'run' ], - env: [{ - name: 'TUNNEL_TOKEN', - valueFrom: { - secretKeyRef: { - name: tunnelSecret.metadata.name, - key: 'TUNNEL_TOKEN' - } - } - }], + envFrom: [{ secretRef: { name: cloudflaredSecret.metadata.name } }], livenessProbe: { - httpGet: { - path: '/ready', - port: 2000 - }, + httpGet: { path: '/ready', port: 2000 }, failureThreshold: 1, initialDelaySeconds: 10, periodSeconds: 10 - }, - readinessProbe: { - httpGet: { - path: '/ready', - port: 2000 - }, - initialDelaySeconds: 10, - periodSeconds: 10 } } ] } } } -}, { provider, dependsOn: tunnelSecret }) +}, { provider, dependsOn: cloudflaredSecret }) new k8s.core.v1.Service(config.identifier, { metadata: { diff --git a/sln/src/FSharp.ViewEngine/paket.references b/sln/src/FSharp.ViewEngine/paket.references index d06254f..322e600 100644 --- a/sln/src/FSharp.ViewEngine/paket.references +++ b/sln/src/FSharp.ViewEngine/paket.references @@ -1 +1,2 @@ +FSharp.Core JetBrains.Annotations