Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.vscode
.claude
.pi*
1 change: 0 additions & 1 deletion AGENTS.md

This file was deleted.

111 changes: 0 additions & 111 deletions CLAUDE.md

This file was deleted.

9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
[![Deploy](https://github.com/meiermade/FSharp.ViewEngine/actions/workflows/deploy.yml/badge.svg)](https://github.com/meiermade/FSharp.ViewEngine/actions/workflows/deploy.yml)
[![NuGet](https://img.shields.io/nuget/v/FSharp.ViewEngine)](https://www.nuget.org/packages/FSharp.ViewEngine)

<p align="center">
<img src="etc/logo.png" alt="FSharp.ViewEngine" width="128">
<p style="text-align:center;">
<img src="etc/logo.svg" alt="FSharp.ViewEngine" width="128">
</p>

# FSharp.ViewEngine
Expand Down Expand Up @@ -98,11 +98,6 @@ html {
## Benchmarks
Ran on February 6, 2026 with BenchmarkDotNet MediumRun only.

Environment:
- Windows 11 (10.0.26200.7623)
- 12th Gen Intel Core i9-12900HK
- .NET SDK 10.0.102, .NET Runtime 10.0.2 (X64 RyuJIT AVX2)

Command:
```
cd sln && dotnet run -c Release --project src/Benchmarks/Benchmarks.fsproj
Expand Down
Binary file removed etc/logo.png
Binary file not shown.
74 changes: 1 addition & 73 deletions etc/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pulumi/Pulumi.prod.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
environment:
- fsharp-view-engine/prod
- fsharpviewengine/prod
config:
pulumi:disable-default-providers: ['*']
1 change: 1 addition & 0 deletions pulumi/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './src/aws'
import './src/cloudflare'
import './src/docker'
import './src/k8s'
10 changes: 10 additions & 0 deletions pulumi/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pulumi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"dependencies": {
"@pulumi/aws": "^7.16.0",
"@pulumi/cloudflare": "^6.10.0",
"@pulumi/docker-build": "^0.0.15",
"@pulumi/kubernetes": "^4.18",
"@pulumi/pulumi": "^3.135",
Expand Down
3 changes: 3 additions & 0 deletions pulumi/src/cloudflare/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './provider'
import './tunnel'
import './record'
6 changes: 6 additions & 0 deletions pulumi/src/cloudflare/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as cloudflare from '@pulumi/cloudflare'
import * as config from '../config'

export const provider = new cloudflare.Provider('default', {
apiToken: config.cloudflareConfig.apiToken
})
22 changes: 22 additions & 0 deletions pulumi/src/cloudflare/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as cloudflare from '@pulumi/cloudflare'
import * as config from '../config'
import * as tunnel from './tunnel'
import { provider } from './provider'

const zone = cloudflare.getZoneOutput({
filter: {
name: config.cloudflareConfig.zoneName,
account: {
id: config.cloudflareConfig.accountId
}
}
}, { provider })

new cloudflare.DnsRecord(config.identifier, {
zoneId: zone.zoneId,
name: config.identifier,
type: 'CNAME',
content: tunnel.tunnelHostname,
proxied: true,
ttl: 1
}, { provider, deleteBeforeReplace: true })
35 changes: 35 additions & 0 deletions pulumi/src/cloudflare/tunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as cloudflare from '@pulumi/cloudflare'
import { provider } from './provider'
import * as config from '../config'

export const tunnel = new cloudflare.ZeroTrustTunnelCloudflared(config.identifier, {
accountId: config.cloudflareConfig.accountId,
name: config.identifier,
configSrc: 'cloudflare'
}, { provider, deleteBeforeReplace: true })

new cloudflare.ZeroTrustTunnelCloudflaredConfig(config.identifier, {
accountId: config.cloudflareConfig.accountId,
tunnelId: tunnel.id,
source: 'cloudflare',
config: {
ingresses: [
{
hostname: `${config.identifier}.${config.cloudflareConfig.zoneName}`,
service: `http://${config.identifier}.${config.k8sConfig.namespace}.svc.cluster.local:80`
},
{
service: 'http_status:404'
}
]
}
}, { provider })

export const tunnelHostname = tunnel.id.apply(id => `${id}.cfargotunnel.com`)

const tunnelTokenRes = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({
accountId: config.cloudflareConfig.accountId,
tunnelId: tunnel.id
}, { provider })

export const tunnelToken = tunnelTokenRes.token
15 changes: 11 additions & 4 deletions pulumi/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import * as path from 'path'

export const rootDir = path.dirname(path.dirname(__dirname))

export const domain = 'fsharpviewengine.meiermade.com'

export const identifier = 'fsharp-view-engine'
export const identifier = 'fsharpviewengine'

const rawAwsConfig = new pulumi.Config('aws')
const rawCloudflareConfig = new pulumi.Config('cloudflare')
const rawK8sConfig = new pulumi.Config('k8s')

export const awsConfig = {
accountId: rawAwsConfig.require('platformAccountId'),
region: rawAwsConfig.require('region'),
eksNodeManagerArn: rawAwsConfig.require('eksNodeManagerArn')
}

export const cloudflareConfig = {
accountId: rawCloudflareConfig.require('accountId'),
apiToken: rawCloudflareConfig.requireSecret('apiToken'),
zoneName: rawCloudflareConfig.require('zoneName'),
cloudflaredVersion: '2026.2.0'
}

export const k8sConfig = {
namespace: rawK8sConfig.get('namespace'),
namespace: rawK8sConfig.require('namespace'),
}
83 changes: 66 additions & 17 deletions pulumi/src/k8s/deployment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as k8s from '@pulumi/kubernetes'
import { provider } from './provider'
import * as image from '../docker/image'
import * as tunnel from '../cloudflare/tunnel'
import * as config from '../config'

let appConfigMap = new k8s.core.v1.ConfigMap(config.identifier, {
Expand All @@ -16,6 +17,16 @@ 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`, {
metadata: {
name: `${config.identifier}-cloudflared`,
namespace: config.k8sConfig.namespace
},
stringData: {
TUNNEL_TOKEN: tunnel.tunnelToken
}
}, { provider })

const deployment = new k8s.apps.v1.Deployment(config.identifier, {
metadata: {
name: config.identifier,
Expand All @@ -27,30 +38,68 @@ const deployment = new k8s.apps.v1.Deployment(config.identifier, {
template: {
metadata: { labels: labels },
spec: {
containers: [{
name: config.identifier,
image: image.imageRef,
imagePullPolicy: 'IfNotPresent',
envFrom: [ { configMapRef: { name: appConfigMap.metadata.name } } ],
livenessProbe: {
httpGet: {
path: '/health',
port: 5000
containers: [
{
name: config.identifier,
image: image.imageRef,
imagePullPolicy: 'IfNotPresent',
envFrom: [ { configMapRef: { name: appConfigMap.metadata.name } } ],
livenessProbe: {
httpGet: {
path: '/health',
port: 5000
},
initialDelaySeconds: 5
},
initialDelaySeconds: 5
readinessProbe: {
httpGet: {
path: '/health',
port: 5000
},
initialDelaySeconds: 5
}
},
readinessProbe: {
httpGet: {
path: '/health',
port: 5000
{
name: 'cloudflared',
image: `cloudflare/cloudflared:${config.cloudflareConfig.cloudflaredVersion}`,
args: [
'tunnel',
'--no-autoupdate',
'--metrics', '0.0.0.0:2000',
'run'
],
env: [{
name: 'TUNNEL_TOKEN',
valueFrom: {
secretKeyRef: {
name: tunnelSecret.metadata.name,
key: 'TUNNEL_TOKEN'
}
}
}],
livenessProbe: {
httpGet: {
path: '/ready',
port: 2000
},
failureThreshold: 1,
initialDelaySeconds: 10,
periodSeconds: 10
},
initialDelaySeconds: 5
readinessProbe: {
httpGet: {
path: '/ready',
port: 2000
},
initialDelaySeconds: 10,
periodSeconds: 10
}
}
}]
]
}
}
}
}, { provider })
}, { provider, dependsOn: tunnelSecret })

new k8s.core.v1.Service(config.identifier, {
metadata: {
Expand Down
Loading
Loading