zuul-operator/conf/zuul/resources.dhall

916 lines
39 KiB
Plaintext

{- Zuul CR kubernetes resources
The evaluation of that file is a function that takes the cr inputs as an argument,
and returns the list of kubernetes of objects.
The resources expect secrets to be created by the zuul ansible role:
* `${name}-gearman-tls` with:
* `ca.pem`
* `server.pem`
* `server.key`
* `client.pem`
* `client.key`
* `${name}-zookeeper-tls` with:
* `ca.crt`
* `tls.crt`
* `tls.key`
* `zk.pem` the keystore
* `${name}-registry-tls` with:
* `cert.pem`
* `cert.key`
* `secret` a password
* `username` the user name with write access
* `password` the user password
* `${name}-database-password` with a `password` key, (unless an input.database db uri is provided).
-}
let Prelude = ../Prelude.dhall
let Kubernetes = ../Kubernetes.dhall
let Schemas = ./input.dhall
let F = ./functions.dhall
let Input = Schemas.Input.Type
let JobVolume = Schemas.JobVolume.Type
let UserSecret = Schemas.UserSecret.Type
let EnvSecret = { name : Text, secret : Text, key : Text }
let File = { path : Text, content : Text }
let Volume =
{ Type = { name : Text, dir : Text, files : List File }
, default.files = [] : List File
}
let {- A high level description of a component such as the scheduler or the launcher
-} Component =
{ Type =
{ name : Text
, count : Natural
, container : Kubernetes.Container.Type
, data-dir : List Volume.Type
, volumes : List Volume.Type
, extra-volumes : List Kubernetes.Volume.Type
, claim-size : Natural
}
, default =
{ data-dir = [] : List Volume.Type
, volumes = [] : List Volume.Type
, extra-volumes = [] : List Kubernetes.Volume.Type
, claim-size = 0
}
}
let {- The Kubernetes resources of a Component
-} KubernetesComponent =
{ Type =
{ Service : Optional Kubernetes.Service.Type
, Deployment : Optional Kubernetes.Deployment.Type
, StatefulSet : Optional Kubernetes.StatefulSet.Type
}
, default =
{ Service = None Kubernetes.Service.Type
, Deployment = None Kubernetes.Deployment.Type
, StatefulSet = None Kubernetes.StatefulSet.Type
}
}
in \(input : Input)
-> let mkVolumeEmptyDir =
Prelude.List.map
Volume.Type
Kubernetes.Volume.Type
( \(volume : Volume.Type)
-> Kubernetes.Volume::{
, name = volume.name
, emptyDir = Some Kubernetes.EmptyDirVolumeSource::{=}
}
)
let mkVolumeSecret =
Prelude.List.map
Volume.Type
Kubernetes.Volume.Type
( \(volume : Volume.Type)
-> Kubernetes.Volume::{
, name = volume.name
, secret = Some Kubernetes.SecretVolumeSource::{
, secretName = Some volume.name
, defaultMode = Some 256
}
}
)
let mkPodTemplateSpec =
\(component : Component.Type)
-> \(labels : F.Labels)
-> Kubernetes.PodTemplateSpec::{
, metadata = F.mkObjectMeta component.name labels
, spec = Some Kubernetes.PodSpec::{
, volumes = Some
( mkVolumeSecret component.volumes
# mkVolumeEmptyDir component.data-dir
# component.extra-volumes
)
, containers = [ component.container ]
, automountServiceAccountToken = Some False
}
}
let mkStatefulSet =
\(component : Component.Type)
-> let labels = F.mkComponentLabel input.name component.name
let component-name = input.name ++ "-" ++ component.name
let claim =
if Natural/isZero component.claim-size
then [] : List Kubernetes.PersistentVolumeClaim.Type
else [ Kubernetes.PersistentVolumeClaim::{
, apiVersion = ""
, kind = ""
, metadata = Kubernetes.ObjectMeta::{
, name = component-name
}
, spec = Some Kubernetes.PersistentVolumeClaimSpec::{
, accessModes = Some [ "ReadWriteOnce" ]
, resources = Some Kubernetes.ResourceRequirements::{
, requests = Some
( toMap
{ storage =
Natural/show
component.claim-size
++ "Gi"
}
)
}
}
}
]
in Kubernetes.StatefulSet::{
, metadata = F.mkObjectMeta component-name labels
, spec = Some Kubernetes.StatefulSetSpec::{
, serviceName = component.name
, replicas = Some component.count
, selector = F.mkSelector labels
, template = mkPodTemplateSpec component labels
, volumeClaimTemplates = Some claim
}
}
let mkDeployment =
\(component : Component.Type)
-> let labels = F.mkComponentLabel input.name component.name
let component-name = input.name ++ "-" ++ component.name
in Kubernetes.Deployment::{
, metadata = F.mkObjectMeta component-name labels
, spec = Some Kubernetes.DeploymentSpec::{
, replicas = Some component.count
, selector = F.mkSelector labels
, template = mkPodTemplateSpec component labels
}
}
let mkEnvVarValue =
Prelude.List.map
F.Label
Kubernetes.EnvVar.Type
( \(env : F.Label)
-> Kubernetes.EnvVar::{
, name = env.mapKey
, value = Some env.mapValue
}
)
let mkEnvVarSecret =
Prelude.List.map
EnvSecret
Kubernetes.EnvVar.Type
( \(env : EnvSecret)
-> Kubernetes.EnvVar::{
, name = env.name
, valueFrom = Some Kubernetes.EnvVarSource::{
, secretKeyRef = Some Kubernetes.SecretKeySelector::{
, key = env.key
, name = Some env.secret
}
}
}
)
let mkVolumeMount =
Prelude.List.map
Volume.Type
Kubernetes.VolumeMount.Type
( \(volume : Volume.Type)
-> Kubernetes.VolumeMount::{
, name = volume.name
, mountPath = volume.dir
}
)
let mkSecret =
\(volume : Volume.Type)
-> Kubernetes.Resource.Secret
Kubernetes.Secret::{
, metadata = Kubernetes.ObjectMeta::{ name = volume.name }
, stringData = Some
( Prelude.List.map
File
{ mapKey : Text, mapValue : Text }
( \(config : File)
-> { mapKey = config.path
, mapValue = config.content
}
)
volume.files
)
}
let zk-conf =
merge
{ None =
[ Volume::{
, name = "${input.name}-secret-zk"
, dir = "/conf-tls"
, files =
[ { path = "zoo.cfg"
, content = ./files/zoo.cfg.dhall "/conf" "/conf"
}
]
}
]
, Some = \(some : UserSecret) -> [] : List Volume.Type
}
input.zookeeper
let zk-client-conf =
merge
{ None =
[ Volume::{
, name = "${input.name}-zookeeper-tls"
, dir = "/etc/zookeeper-tls"
}
]
, Some = \(some : UserSecret) -> [] : List Volume.Type
}
input.zookeeper
let zk-hosts-zuul =
merge
{ None =
''
hosts=zk:2281
tls_cert=/etc/zookeeper-tls/tls.crt
tls_key=/etc/zookeeper-tls/tls.key
tls_ca=/etc/zookeeper-tls/ca.crt
''
, Some = \(some : UserSecret) -> "hosts=%(ZUUL_ZK_HOSTS)"
}
input.zookeeper
let zk-hosts-nodepool =
merge
{ None =
''
zookeeper-servers:
- host: zk
port: 2281
zookeeper-tls:
cert: /etc/zookeeper-tls/tls.crt
key: /etc/zookeeper-tls/tls.key
ca: /etc/zookeeper-tls/ca.crt
''
, Some =
\(some : UserSecret)
-> ''
zookeeper-servers:
- hosts: %(ZUUL_ZK_HOSTS)"
''
}
input.zookeeper
let {- Add support for TLS protected external zookeeper service
-} zk-hosts-secret-env =
merge
{ None = [] : List Kubernetes.EnvVar.Type
, Some =
\(some : UserSecret)
-> mkEnvVarSecret
[ { name = "ZUUL_ZK_HOSTS"
, secret = some.secretName
, key = F.defaultText some.key "hosts"
}
]
}
input.zookeeper
let db-internal-password-env =
\(env-name : Text)
-> mkEnvVarSecret
[ { name = env-name
, secret = "${input.name}-database-password"
, key = "password"
}
]
let org = "docker.io/zuul"
let version = "latest"
let image = \(name : Text) -> "${org}/${name}:${version}"
let etc-zuul =
Volume::{
, name = input.name ++ "-secret-zuul"
, dir = "/etc/zuul"
, files =
[ { path = "zuul.conf"
, content = ./files/zuul.conf.dhall input zk-hosts-zuul
}
]
}
let etc-zuul-registry =
Volume::{
, name = input.name ++ "-secret-registry"
, dir = "/etc/zuul"
, files =
[ { path = "registry.yaml"
, content =
let public-url =
F.defaultText
input.registry.public-url
"https://registry:9000"
in ./files/registry.yaml.dhall public-url
}
]
}
let etc-nodepool =
Volume::{
, name = input.name ++ "-secret-nodepool"
, dir = "/etc/nodepool"
, files =
[ { path = "nodepool.yaml"
, content = ./files/nodepool.yaml.dhall zk-hosts-nodepool
}
]
}
let Components =
{ Backend =
let db-volumes =
[ Volume::{ name = "pg-data", dir = "/var/lib/pg/" } ]
let zk-volumes =
[ Volume::{
, name = "zk-log"
, dir = "/var/log/zookeeper/"
}
, Volume::{
, name = "zk-dat"
, dir = "/var/lib/zookeeper/"
}
]
in { Database =
merge
{ None = KubernetesComponent::{
, Service = Some
(F.mkService input.name "db" "pg" 5432)
, StatefulSet = Some
( mkStatefulSet
Component::{
, name = "db"
, count = 1
, data-dir = db-volumes
, claim-size = 1
, container = Kubernetes.Container::{
, name = "db"
, image = Some
"docker.io/library/postgres:12.1"
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "pg"
, containerPort = 5432
}
]
, env = Some
( mkEnvVarValue
( toMap
{ POSTGRES_USER = "zuul"
, PGDATA =
"/var/lib/pg/data"
}
)
# db-internal-password-env
"POSTGRES_PASSWORD"
)
, volumeMounts = Some
(mkVolumeMount db-volumes)
}
}
)
}
, Some =
\(some : UserSecret)
-> KubernetesComponent.default
}
input.database
, ZooKeeper =
merge
{ None = KubernetesComponent::{
, Service = Some
(F.mkService input.name "zk" "zk" 2281)
, StatefulSet = Some
( mkStatefulSet
Component::{
, name = "zk"
, count = 1
, data-dir = zk-volumes
, volumes = zk-conf # zk-client-conf
, claim-size = 1
, container = Kubernetes.Container::{
, name = "zk"
, command = Some
[ "sh"
, "-c"
, "cp /conf-tls/zoo.cfg /conf/ && "
++ "cp /etc/zookeeper-tls/zk.pem /conf/zk.pem && "
++ "cp /etc/zookeeper-tls/ca.crt /conf/ca.pem && "
++ "chown zookeeper /conf/zoo.cfg /conf/zk.pem /conf/ca.pem && "
++ "exec /docker-entrypoint.sh zkServer.sh start-foreground"
]
, image = Some
"docker.io/library/zookeeper"
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "zk"
, containerPort = 2281
}
]
, volumeMounts = Some
( mkVolumeMount
( zk-volumes
# zk-conf
# zk-client-conf
)
)
}
}
)
}
, Some =
\(some : UserSecret)
-> KubernetesComponent.default
}
input.zookeeper
}
, Zuul =
let zuul-image =
\(name : Text) -> Some (image ("zuul-" ++ name))
let zuul-env =
mkEnvVarValue (toMap { HOME = "/var/lib/zuul" })
let db-secret-env =
merge
{ None = db-internal-password-env "ZUUL_DB_PASSWORD"
, Some =
\(some : UserSecret)
-> mkEnvVarSecret
[ { name = "ZUUL_DB_URI"
, secret = some.secretName
, key = F.defaultText some.key "db_uri"
}
]
}
input.database
let {- executor and merger do not need database info, but they fail to parse config without the env variable
-} db-nosecret-env =
mkEnvVarValue (toMap { ZUUL_DB_PASSWORD = "unused" })
let zuul-data-dir =
[ Volume::{ name = "zuul-data", dir = "/var/lib/zuul" }
]
let sched-config =
Volume::{
, name = input.scheduler.config.secretName
, dir = "/etc/zuul-scheduler"
}
let gearman-config =
Volume::{
, name = input.name ++ "-gearman-tls"
, dir = "/etc/zuul-gearman"
}
let executor-ssh-key =
Volume::{
, name = input.executor.ssh_key.secretName
, dir = "/etc/zuul-executor"
}
let zuul-volumes =
[ etc-zuul, gearman-config ] # zk-client-conf
let web-volumes = zuul-volumes
let merger-volumes = zuul-volumes
let scheduler-volumes = zuul-volumes # [ sched-config ]
let executor-volumes = zuul-volumes # [ executor-ssh-key ]
in { Scheduler = KubernetesComponent::{
, Service = Some
(F.mkService input.name "scheduler" "gearman" 4730)
, StatefulSet = Some
( mkStatefulSet
Component::{
, name = "scheduler"
, count = 1
, data-dir = zuul-data-dir
, volumes = scheduler-volumes
, claim-size = 5
, container = Kubernetes.Container::{
, name = "scheduler"
, image = zuul-image "scheduler"
, args = Some [ "zuul-scheduler", "-d" ]
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "gearman"
, containerPort = 4730
}
]
, env = Some
( zuul-env
# db-secret-env
# zk-hosts-secret-env
)
, volumeMounts = Some
( mkVolumeMount
(scheduler-volumes # zuul-data-dir)
)
}
}
)
}
, Executor = KubernetesComponent::{
, Service = Some
(F.mkService input.name "executor" "finger" 7900)
, StatefulSet = Some
( mkStatefulSet
Component::{
, name = "executor"
, count = 1
, data-dir = zuul-data-dir
, volumes = executor-volumes
, extra-volumes =
let job-volumes =
F.mkJobVolume
Kubernetes.Volume.Type
( \(job-volume : JobVolume)
-> job-volume.volume
)
input.jobVolumes
in job-volumes
, claim-size = 0
, container = Kubernetes.Container::{
, name = "executor"
, image = zuul-image "executor"
, args = Some [ "zuul-executor", "-d" ]
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "finger"
, containerPort = 7900
}
]
, env = Some (zuul-env # db-nosecret-env)
, volumeMounts =
let job-volumes-mount =
F.mkJobVolume
Volume.Type
( \(job-volume : JobVolume)
-> Volume::{
, name =
job-volume.volume.name
, dir = job-volume.dir
}
)
input.jobVolumes
in Some
( mkVolumeMount
( executor-volumes
# zuul-data-dir
# job-volumes-mount
)
)
, securityContext = Some Kubernetes.SecurityContext::{
, privileged = Some True
}
}
}
)
}
, Web = KubernetesComponent::{
, Service = Some
(F.mkService input.name "web" "api" 9000)
, Deployment = Some
( mkDeployment
Component::{
, name = "web"
, count = 1
, data-dir = zuul-data-dir
, volumes = web-volumes
, container = Kubernetes.Container::{
, name = "web"
, image = zuul-image "web"
, args = Some [ "zuul-web", "-d" ]
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "api"
, containerPort = 9000
}
]
, env = Some
( zuul-env
# db-secret-env
# zk-hosts-secret-env
)
, volumeMounts = Some
( mkVolumeMount
(web-volumes # zuul-data-dir)
)
}
}
)
}
, Merger = KubernetesComponent::{
, Deployment = Some
( mkDeployment
Component::{
, name = "merger"
, count = 1
, data-dir = zuul-data-dir
, volumes = merger-volumes
, container = Kubernetes.Container::{
, name = "merger"
, image = zuul-image "merger"
, args = Some [ "zuul-merger", "-d" ]
, imagePullPolicy = Some "IfNotPresent"
, env = Some (zuul-env # db-nosecret-env)
, volumeMounts = Some
( mkVolumeMount
(merger-volumes # zuul-data-dir)
)
}
}
)
}
, Registry =
let registry-volumes =
[ etc-zuul-registry
, Volume::{
, name = input.name ++ "-registry-tls"
, dir = "/etc/zuul-registry"
}
]
let registry-env =
mkEnvVarSecret
( Prelude.List.map
Text
EnvSecret
( \(key : Text)
-> { name = "ZUUL_REGISTRY_${key}"
, key = key
, secret =
input.name ++ "-registry-tls"
}
)
[ "secret", "username", "password" ]
)
in KubernetesComponent::{
, Service = Some
( F.mkService
input.name
"registry"
"registry"
9000
)
, StatefulSet = Some
( mkStatefulSet
Component::{
, name = "registry"
, count =
F.defaultNat input.registry.count 0
, data-dir = zuul-data-dir
, volumes = registry-volumes
, claim-size =
F.defaultNat
input.registry.storage-size
20
, container = Kubernetes.Container::{
, name = "registry"
, image = zuul-image "registry"
, args = Some
[ "zuul-registry"
, "-c"
, "/etc/zuul/registry.yaml"
, "serve"
]
, imagePullPolicy = Some "IfNotPresent"
, ports = Some
[ Kubernetes.ContainerPort::{
, name = Some "registry"
, containerPort = 9000
}
]
, env = Some registry-env
, volumeMounts = Some
( mkVolumeMount
( registry-volumes
# zuul-data-dir
)
)
}
}
)
}
}
, Nodepool =
let nodepool-image =
\(name : Text) -> Some (image ("nodepool-" ++ name))
let nodepool-data-dir =
[ Volume::{
, name = "nodepool-data"
, dir = "/var/lib/nodepool"
}
]
let nodepool-config =
Volume::{
, name = input.launcher.config.secretName
, dir = "/etc/nodepool-config"
}
let openstack-config =
merge
{ None = [] : List Volume.Type
, Some =
\(some : UserSecret)
-> [ Volume::{
, name = some.secretName
, dir = "/etc/nodepool-openstack"
}
]
}
input.externalConfig.openstack
let kubernetes-config =
merge
{ None = [] : List Volume.Type
, Some =
\(some : UserSecret)
-> [ Volume::{
, name = some.secretName
, dir = "/etc/nodepool-kubernetes"
}
]
}
input.externalConfig.kubernetes
let nodepool-env =
mkEnvVarValue
( toMap
{ HOME = "/var/lib/nodepool"
, OS_CLIENT_CONFIG_FILE =
"/etc/nodepool-openstack/"
++ F.defaultKey
input.externalConfig.openstack
"clouds.yaml"
, KUBECONFIG =
"/etc/nodepool-kubernetes/"
++ F.defaultKey
input.externalConfig.kubernetes
"kube.config"
}
)
let nodepool-volumes =
[ etc-nodepool, nodepool-config ]
# openstack-config
# kubernetes-config
# zk-client-conf
let shard-config =
"cat /etc/nodepool/nodepool.yaml /etc/nodepool-config/*.yaml > /var/lib/nodepool/config.yaml; "
in { Launcher = KubernetesComponent::{
, Deployment = Some
( mkDeployment
Component::{
, name = "launcher"
, count = 1
, data-dir = nodepool-data-dir
, volumes = nodepool-volumes
, container = Kubernetes.Container::{
, name = "launcher"
, image = nodepool-image "launcher"
, args = Some
[ "sh"
, "-c"
, shard-config
++ "nodepool-launcher -d -c /var/lib/nodepool/config.yaml"
]
, imagePullPolicy = Some "IfNotPresent"
, env = Some nodepool-env
, volumeMounts = Some
( mkVolumeMount
(nodepool-volumes # nodepool-data-dir)
)
}
}
)
}
}
}
let {- This function transforms the different types into the Kubernetes.Resource
union to enable using them inside a single List array
-} mkUnion =
\(component : KubernetesComponent.Type)
-> let empty = [] : List Kubernetes.Resource
in merge
{ None = empty
, Some =
\(some : Kubernetes.Service.Type)
-> [ Kubernetes.Resource.Service some ]
}
component.Service
# merge
{ None = empty
, Some =
\(some : Kubernetes.StatefulSet.Type)
-> [ Kubernetes.Resource.StatefulSet some ]
}
component.StatefulSet
# merge
{ None = empty
, Some =
\(some : Kubernetes.Deployment.Type)
-> [ Kubernetes.Resource.Deployment some ]
}
component.Deployment
in { Components = Components
, List =
{ apiVersion = "v1"
, kind = "List"
, items =
Prelude.List.map
Volume.Type
Kubernetes.Resource
mkSecret
( zk-conf
# [ etc-zuul, etc-nodepool, etc-zuul-registry ]
)
# mkUnion Components.Backend.Database
# mkUnion Components.Backend.ZooKeeper
# mkUnion Components.Zuul.Scheduler
# mkUnion Components.Zuul.Executor
# mkUnion Components.Zuul.Web
# mkUnion Components.Zuul.Merger
# mkUnion Components.Zuul.Registry
# mkUnion Components.Nodepool.Launcher
}
}