1open module Service
2
3import "site.pkl" as site
4
5const nomadHostPort = "{{ if regexMatch \":\" .Address }}[{{ .Address }}]{{ else }}{{ .Address }}{{ end }}:{{ .Port }}"
6
7/// Name shown in the management UI and service listings.
8class Metadata {
9 name: String
10 tagline: String = ""
11 launcher: Boolean = true
12 /// Discovery group; absent uses the public route's auth role.
13 access: String?
14}
15
16/// One HTTP listener and its public route.
17class Http {
18 containerPort: UInt16
19 hostPort: UInt16?
20 loopback: Boolean = false
21 subdomain: String?
22 authRole: String?
23 /// Backend header set from the authenticated OIDC preferred username.
24 userHeader: String?
25 /// Identity headers copied from a valid SSO session; anonymous requests continue.
26 identityHeaders: Mapping<String, String> = new {}
27 /// Set X-Real-Ip from Caddy's observed client address before proxying.
28 forwardRealIp: Boolean = false
29 /// Generated secret whose value becomes the private proxy header name.
30 identityProofSecret: String?
31 /// Additional hosts routed to this container without SSO headers.
32 plainHostnames: Listing<String> = new {}
33 hostname: String? = read?("prop:hostname") ?? if (subdomain != null) "\(subdomain).\(site.domain)" else null
34 tlsInternal: Boolean = site.tlsInternal
35 checkPath: String = "/"
36 /// Internal Prometheus endpoint collected by the home server dashboard.
37 metricsPath: String?
38 checkHeaders: Mapping<String, String> = new {}
39 /// Request path to a file or directory in this service's folder.
40 overrideFiles: Mapping<String, String> = new {}
41 /// HTML inserted before </head> for the listed page paths.
42 headHtml: Mapping<String, String> = new {}
43}
44
45/// One TCP listener published through Nomad.
46class Tcp {
47 name: String
48 containerPort: UInt16
49 hostPort: UInt16?
50 loopback: Boolean = true
51}
52
53/// A container mount, keyed by its absolute path inside the container.
54class Volume {
55 /// Host source; absent means the same path beneath this service's data root.
56 src: String?
57 /// File or directory relative to this service's assets folder; copied into a read-only mount.
58 config: String?
59 readOnly: Boolean = config != null
60}
61
62/// A persistent value generated when the service is first provisioned.
63class GeneratedSecret {
64 bytes: Int(isBetween(16, 128)) = 32
65}
66
67/// Provider-owned resource request; the alias names its allocated secret.
68open class Requirement {
69 alias: String
70 fixed provider: String
71 fixed kind: String
72}
73
74/// One Podman task. A service can contain one or several of these.
75class Container {
76 /// Use either an image or a build directory in this service's release folder.
77 image: String?
78 build: String?
79 entrypoint: String?
80 http: Http?
81 tcp: Tcp?
82 volumes: Mapping<String, Volume> = new {}
83 /// Podman tmpfs mounts for data that must not persist in service storage.
84 tmpfs: Listing<String> = new {}
85 /// `${secret.own.key}` and `${secret.alias.key}` resolve from Nomad variables.
86 env: Mapping<String, String> = new {}
87 /// Nomad template for environment values resolved after allocation.
88 envTemplate: String?
89 args: Listing<String> = new {}
90 capAdd: Listing<String> = new {}
91 devices: Listing<String> = new {}
92 extraHosts: Listing<String> = new {}
93 hostNetwork: Boolean = false
94 imageUser: Boolean = false
95 /// Prestart tasks finish first; prestart sidecars stay up for the main tasks.
96 lifecycle: "main"|"prestart"|"prestartSidecar" = "main"
97 rootGroup: Boolean = false
98 cpu: Int = 200
99 memory: Int = 512
100}
101
102/// Stable internal ID supplied from the service definition name by the caller.
103id: String = read?("prop:serviceId")
104/// Stable numeric owner assigned by the deployment tool.
105uid: Int = read("prop:uid").toInt()
106meta: Metadata
107/// Allows site-specific services to stay out of deployments where they would cause side effects.
108enabled: Boolean = true
109/// Simple replaces one allocation; overlapped runs a canary alongside it.
110rollout: String = "simple"
111/// Fresh previews create empty storage and new secrets instead of forking production.
112stageIsolation: "clone"|"fresh" = "clone"
113/// Time allowed for a new allocation to pass its service checks.
114healthyDeadline: String?
115/// Delay before persistent health-check failures may restart a running allocation.
116healthRestartGrace: String?
117/// Repeatable service configuration run after a healthy deployment.
118setup: String?
119/// Service-owned data preparation before its container starts.
120prepare: String?
121/// Handler for input requests owned by this service.
122provide: String?
123
124/// Use this for one container; it becomes the "app" task in the output.
125container: Container?
126
127/// Use this instead of `container` when the service has named tasks.
128containers: Mapping<String, Container>?
129
130secrets: Mapping<String, GeneratedSecret> = new {}
131/// Secret names supplied from outside the release, in import-file line order.
132requiredSecrets: Listing<String> = new {}
133requirements: Listing<Requirement> = new {}
134/// Service startup dependency with no resource to allocate.
135dependsOn: Listing<String> = new {}
136/// Resource service.name emitted by this app's trace exporter.
137traceServiceName: String?
138/// Metrics sent by the app over OTLP instead of a Prometheus HTTP endpoint.
139metricsPushed: Boolean = false
140
141local deploymentContainers: Mapping<String, Container> =
142 if (!enabled)
143 new Mapping {}
144 else if (container != null && containers != null)
145 throw("Declare either container or containers, not both")
146 else if (container != null)
147 new Mapping { ["app"] = container!! }
148 else if (containers != null && !containers!!.isEmpty)
149 containers!!
150 else if (!requirements.isEmpty)
151 new Mapping {}
152 else
153 throw("Declare a container or an input")
154
155local vmStartupGrace: String? = if (read?("env:STUDIO_VM_ACCEL") == "qemu") "60m" else null
156
157output {
158 renderer = new JsonRenderer {}
159 value = new {
160 id = module.id
161 name = module.meta.name
162 tagline = module.meta.tagline
163 launcher = module.meta.launcher
164 access = module.meta.access
165 rollout = module.rollout
166 stageIsolation = module.stageIsolation
167 healthyDeadline = vmStartupGrace ?? module.healthyDeadline
168 healthRestartGrace = vmStartupGrace ?? module.healthRestartGrace
169 hostRoot = "\(site.root)/prod/\(module.id)"
170 stagingRoot = "\(site.root)/staging"
171 pool = site.pool
172 cloverRoot = site.cloverRoot
173 cloverGid = site.cloverGid
174 mediaRoot = site.mediaRoot
175 ownerEmail = site.ownerEmail
176 containers = deploymentContainers
177 setup = module.setup
178 prepare = module.prepare
179 provide = module.provide
180 // External mounts do not require this service's dataset.
181 hasManagedVolumes = deploymentContainers.toMap().values.any((task) ->
182 task.volumes.toMap().values.any((volume) -> volume.src == null && volume.config == null))
183 secrets = module.secrets
184 requiredSecrets = module.requiredSecrets
185 requirements = module.requirements
186 dependsOn = module.dependsOn
187 traceServiceName = module.traceServiceName
188 metricsPushed = module.metricsPushed
189 }
190}