Helm chart and values

Runs myriarchd (the myriarch server) with high availability: replicas: 2 (all replicas serve agents and the API; the singleton jobs run on the leader elected through a PostgreSQL advisory lock), a Service (8080 HTTP/UI/REST/OpAMP, 9090 metrics), a PodDisruptionBudget, a NetworkPolicy, a ConfigMap with myriarchd.yaml, and secrets mounted as files. Access from outside the cluster is optional: Gateway API HTTPRoute (recommended), Ingress, or an OpenShift Route. Failover and shutdown timing are in High availability.

The chart installs with its defaults, never starts the dev mode and has a login without an enterprise identity provider (the bootstrap admin). Single sign-on with any OpenID Connect provider is a configuration change: docs/operations/authentication.md.

Available from v0.2.0. The values are validated against a strict schema, values.schema.json: helm install, upgrade, template and lint refuse a key that the chart does not know, a value of the wrong type and a malformed value, before anything is rendered. The chart of v0.1.0 has no schema and ignores such a key without a message. See Values schema.

Minimal installation

Two things are needed: a Secret with the database DSN and the external URL.

kubectl create namespace myriarch
kubectl -n myriarch create secret generic myriarch-secrets \
  --from-literal=database-dsn='postgres://myriarch:<password>@db.example.com:5432/myriarch?sslmode=verify-full'
# from a checkout of the repository, or, from v0.2.0, from the signed OCI chart (verify it first, see "Signed OCI chart" below):
helm install myriarch deploy/helm/myriarch -n myriarch --set config.server.publicUrl=https://fleet.example.com
helm install myriarch oci://ghcr.io/myriarch/charts/myriarch --version 0.2.1 -n myriarch --set config.server.publicUrl=https://fleet.example.com

The documented release name and namespace are both myriarch, so the Service, Deployment, ConfigMap and ServiceAccount are all called myriarch.

Signed OCI chart

Available from v0.2.0. The chart of v0.1.0 is installed from a checkout of the repository.

Every release publishes the chart as oci://ghcr.io/myriarch/charts/myriarch; its version and its appVersion are the version of the release. The release workflow writes the digest of the signed server image into image.digest before it packages the chart, so the published chart references the image by digest, signs the chart keyless with the identity of the release workflow, and checks before publication that helm template renders every image by that digest. In the repository image.digest stays empty (the image is then appVersion’s tag). Verification commands: Verify a release; installation: Install with Helm.

Access

Without Ingress, HTTPRoute or Route nothing is exposed. Use a port-forward (the NetworkPolicy does not affect it):

kubectl -n myriarch port-forward svc/myriarch 8080:8080     # http://localhost:8080

For access from outside the cluster choose one:

PathValuesNotes
Gateway API (recommended)httpRoute.enabled, parentRefs, hostnames, tlsTerminatedAtGatewaythe chart creates the HTTPRoute, not the Gateway
Ingressingress.enabled, className, annotations, tlsingress-nginx is no longer maintained since March 2026; examples for maintained controllers are in examples/ingress-*.yaml
OpenShift Routeopenshift.enabled=trueRoute with edge TLS, see “Profiles”

Ingress and HTTPRoute need config.server.trustedProxies (CIDR of the controller or Gateway data plane) and networkPolicy.ingress.from (their namespace) and networkPolicy.ingress.allowSameNamespace=false.

First login

The default login is the bootstrap admin, a break-glass administrator that is configured declaratively (bootstrapAdmin). The chart generates the Secret myriarch-bootstrap with a one-time token; the install notes print the command to read it (the notes never print a secret):

kubectl -n myriarch get secret myriarch-bootstrap -o jsonpath='{.data.token}' | base64 -d

Open the UI and paste the token into the bootstrap admin form (it sits behind “Use the bootstrap admin” when single sign-on is offered as well). The token works for one sign-in. For a new one delete the Secret and run helm upgrade. GitOps tools that render offline (helm template, Argo CD) cannot see existing Secrets and would generate a new token on every render: use bootstrapAdmin.existingSecret there.

For a durable break-glass account use a bcrypt password hash in your own Secret:

printf '%s' "$PASSWORD" | myriarchd auth hash-password > password-hash       # 12 to 72 characters, bcrypt cost 12
kubectl -n myriarch create secret generic my-bootstrap --from-file=password-hash=password-hash
helm upgrade myriarch ... --set bootstrapAdmin.existingSecret=my-bootstrap \
  --set bootstrapAdmin.tokenKey= --set bootstrapAdmin.passwordHashKey=password-hash

The bootstrap admin is meant for the initial setup: configure config.auth.oidc and set bootstrapAdmin.enabled=false. With OIDC enabled the chart does not mount its credentials (and the server deactivates the account) unless bootstrapAdmin.keepWithOidc=true. Every sign-in is audited as auth.bootstrap.login.

Restrict network access to the bootstrap login. The account has an account-wide backoff: after five failures in a row the server delays every further attempt, doubling the delay up to 15 minutes, whoever sends it. Anyone who can reach POST /api/v1/auth/bootstrap-login can therefore keep the break-glass login in these delays with wrong credentials, and the legitimate administrator gets 429 as well. Allow this path only from the networks of your administrators (a rule for the path at the ingress controller, Gateway or WAF, or block it at the edge and use kubectl port-forward when the break-glass account is needed). The counter is held in memory per replica, so restarting the pods clears it: kubectl -n myriarch rollout restart deployment/myriarch. Details and the audit entries to watch: docs/operations/authentication.md.

Single sign-on with Dex (quickstart)

examples/quickstart-dex.yaml (values for this chart) and examples/dex/dex.yaml (values for the upstream Dex chart) set up Dex as a demo identity provider with one static user:

helm upgrade --install dex dex/dex -n dex -f deploy/helm/myriarch/examples/dex/dex.yaml     # see the header of the file
helm upgrade --install myriarch deploy/helm/myriarch -n myriarch -f deploy/helm/myriarch/examples/quickstart-dex.yaml

The files contain placeholders only (<bcrypt hash>, <client secret>); replace them and never commit real values. For Keycloak, Okta, Entra ID, Google, authentik and Zitadel see docs/operations/authentication.md.

Single sign-on switched on through the environment

The chart reads config.auth.oidc.enabled to decide whether a login path exists and whether the bootstrap admin is active (its Secret is mounted only then). If your platform switches single sign-on on with environment variables instead (MYRIARCH_AUTH_OIDC_ENABLED=true in extraEnv, or injected from a ConfigMap or a Secret), the chart cannot see it, and with bootstrapAdmin.enabled=false it stops with bootstrapAdmin.enabled: no login path is configured. Declare the environment with oidcFromEnvironment:

oidcFromEnvironment: true        # the chart treats OIDC as enabled; the server reads the environment
bootstrapAdmin:
  enabled: false
extraEnv:
  - {name: MYRIARCH_AUTH_OIDC_ENABLED, value: "true"}
  - {name: MYRIARCH_AUTH_OIDC_CLIENTID, value: myriarch}

oidcFromEnvironment only declares what the chart assumes: no login path error, no bootstrap Secret unless bootstrapAdmin.keepWithOidc=true, and the config.auth.oidc.*File fields stay in the rendered configuration (set an empty value to drop one, for example clientSecretFile when the client secret comes from the environment). The server reads the environment at runtime and still refuses to start when no login path remains.

Declarative enrollment tokens

Tokens for agents that exist from the first start, without the UI (enrollmentTokens):

enrollmentTokens:
  - name: gateway            # DNS label, unique; the file is /etc/myriarchd/enrollment/gateway
    labels: {site: hq}       # copied to the agents at registration
    expiresAt: ""            # RFC 3339, empty = no expiry
    # existingSecret: ""     # empty = the chart generates <fullname>-enrollment-<name> (myre_ + 40 characters)
    # key: token

The chart renders opamp.enrollmentTokens and mounts the Secrets as one projected volume. Agents in the cluster can mount the same Secret. The server stores only the SHA-256 hash of each token. Generated values survive upgrades; the GitOps caveat above applies.

Prerequisites

  1. External PostgreSQL (>= 14) with a database and a user. myriarchd runs migrations itself (database.migrateOnStart, protected by an advisory lock, so several replicas are safe). No PgBouncer in transaction pooling mode: LISTEN/NOTIFY and session advisory locks (leader election) need a direct connection or session pooling.
  2. Secret (existingSecret, default myriarch-secrets) with at least database-dsn. Optional keys, depending on the configuration: oidc-client-secret (or oidc-client-cert and oidc-client-key), download-token-secret, git-token, gitops-webhook-secret, github-token, mirror-token, mirror-password, notification URLs. It is mounted completely at /etc/myriarchd/secrets (mode 0440); the configuration points to the files through its *File fields. Executor secrets (webhook/AWX tokens) are passed as environment variables through secretEnv.
  3. Access with WebSocket support and long timeouts for the OpAMP connections (see “Access”). Session affinity is not needed.
  4. Kubernetes >= 1.30 (native preStop sleep action; the distroless image has no sleep; Chart.yaml enforces this through kubeVersion).

Profiles: standard Kubernetes and OpenShift

openshift.enabled selects the profile:

ValueMeaning
false (default)standard profile: fixed user and group 65532 and fsGroup 65532
trueOpenShift profile
autoOpenShift profile when the cluster offers the API security.openshift.io/v1

Caveat for auto: helm template has no cluster access and silently renders the standard profile unless you pass --api-versions security.openshift.io/v1. GitOps tools that template offline need the same flag, or use true/false.

The OpenShift profile

  • drops runAsUser, runAsGroup and fsGroup from the pod security context (restricted-v2 assigns them from the project range; runAsNonRoot and the seccomp profile stay; no runAsUser appears anywhere in the Deployment),
  • creates a Route (openshift.route.*): edge TLS, the host of openshift.route.host or of config.server.publicUrl, router timeouts haproxy.router.openshift.io/timeout and timeout-tunnel (openshift.route.timeout, default 3600s for the long-lived OpAMP WebSockets), insecureEdgeTerminationPolicy, optional externalCertificateSecret,
  • adds the router namespaces (openshift.routerNamespaceSelector) as source of port 8080 and uses the OpenShift DNS rule (openshift.dns: namespace openshift-dns, ports 5353 and 53).

The Route does not require trustedProxies, but without it all clients appear with the address of the router. Set it to the pod CIDR of the router and networkPolicy.ingress.allowSameNamespace=false. examples/openshift.yaml is an overlay of the production example. Every container has requests and limits for cpu, memory and ephemeral-storage, as LimitRanges on OpenShift usually demand.

Network policy

networkPolicy.enabled (default) restricts the pods. A CNI with NetworkPolicy support is a prerequisite.

  • Port 8080 (UI, REST, OpAMP) is reachable from: the own namespace (networkPolicy.ingress.allowSameNamespace, default true; agents and collector gateways next to the pods), plus networkPolicy.ingress.from, plus the router namespaces in the OpenShift profile with a Route. An empty set means no rule for 8080 (denied); kubectl port-forward still works. allowSameNamespace must be false while config.server.trustedProxies or config.opamp.clientCertHeader is set (rendering fails otherwise): any pod of the namespace could forge the forwarded headers.
  • The Pod of helm test (Helm test, tests.enabled, default true) gets a source for port 8080 of its own when allowSameNamespace is false: a pod selector with its labels. The chart adds it only while neither config.server.trustedProxies nor config.opamp.clientCertHeader is set, because every pod that carries these labels could use it (Network access of the test). To keep the policy of v0.1.0, in which no pod of the namespace reaches port 8080 unless networkPolicy.ingress.from names it, set tests.enabled: false.
  • Port 9090 (/metrics, unauthenticated) only from networkPolicy.metrics.from; empty means nobody.
  • Egress (networkPolicy.egress.enabled): DNS (egress.dns; the OpenShift profile uses openshift.dns, UDP and TCP per port), and one rule for TCP egress.ports (default 5432 PostgreSQL, 443 HTTPS) plus egress.extraPorts plus the Git SSH port (22, or the port of an ssh://host:port URL; for ssh:// and scp-like user@host:path URLs while config.gitops.enabled), duplicates removed. Destinations are all addresses except egress.exceptCidrs (cloud metadata service) and every pod of the cluster (PostgreSQL or an IdP such as Dex inside the cluster). egress.extraRules are appended in NetworkPolicy format. In addition myriarchd checks every outbound connection against config.outbound.allowedCidrs itself.

Storage, resources and secrets

  • emptyDir.sizeLimit (default 4Gi) holds the GitOps clone and the artifact cache of each replica; config.artifacts.maxCacheBytes (default 3 GiB) must stay below it. resources.limits.ephemeral-storage (default 5Gi) must stay above emptyDir.sizeLimit plus the 256Mi tmp volume plus the container logs, otherwise the kubelet evicts the pod. With persistence.enabled a shared PVC (persistence.size, ReadWriteMany) is mounted instead.
  • replicaCount defaults to 2. The PodDisruptionBudget is created only while replicaCount is greater than 1 (with one replica minAvailable: 1 would block every node drain); podDisruptionBudget.maxUnavailable replaces minAvailable when it is set.
  • secretChecksum.enabled (default) adds the annotation checksum/secret to the pods: the SHA-256 over the uid and resourceVersion of every Secret the pod mounts (existingSecret, the bootstrap Secret, the enrollment Secrets) as seen through lookup, so a changed Secret rolls the pods on helm upgrade (no digest of the secret data is stored in the Deployment). It is constant under helm template. The first upgrade after an installation with generated Secrets restarts the pods once. An upgrade that creates a generated Secret (a new enrollment token entry, or a generated Secret you deleted to rotate it) restarts the pods too: the checksum then contains the release revision, so even a second rotation rolls the pods (an enrollment token is reconciled at startup). Changes to config: roll the pods through checksum/config.
  • Mounted files: /etc/myriarchd/secrets/<key> (existingSecret), /etc/myriarchd/bootstrap/token and /etc/myriarchd/bootstrap/password-hash (Secret bootstrapAdmin.existingSecret or the generated <fullname>-bootstrap), /etc/myriarchd/enrollment/<name> (one projected volume). All with mode 0440, read-only.
  • *File fields of disabled features are not rendered: auth.oidc.*File while OIDC is off (oidcFromEnvironment=true counts as on), the gitops token, key, CA and webhook secret while gitops is off, releases.tokenFile while releases is off; empty *File values are dropped. config.auth.downloadTokenSecretFile is optional (empty = a random key stored in the database and shared by all replicas).
  • config.executors.allowedWebhookHosts empty means fail closed: every webhook and AWX target is refused (myriarchd still starts). List the permitted hosts (host, host:port, *.example.com, http://host for plain HTTP) and bind each executor secret to its hosts in config.executors.secretEnvHosts.

Operation

  • Rolling update: maxUnavailable: 0, preStop 10 s, terminationGracePeriodSeconds: 60; agents reconnect with backoff through other replicas. Exception for the upgrade to migration 00003: replicas of an older version can no longer store OIDC users afterwards; for this upgrade set replicaCount=1 and scale up again afterwards.
  • Upgrade and rollback: back up the PostgreSQL database before every upgrade. From v0.2.0 on the server checks the schema at start (config.database.migrateOnStart stays true): it refuses to start on a database that a newer version has migrated in a way it cannot work with (the error names the schema version and the compatibility levels), and it starts with a warning on a newer schema that is still compatible. helm rollback therefore works as long as the migrations since the revision were compatible; otherwise the old pods stop at start and you restore the backup. A rollback to v0.1.0 is not supported: it does not check the schema, so going back means restoring the backup. See Server behaviour and ADR-0036.
  • Security: non-root, readOnlyRootFilesystem, all capabilities dropped, seccomp RuntimeDefault; only /var/lib/myriarchd (GitOps clone, artifact cache) and /tmp are writable. The image creates /var/lib/myriarchd (0750, owned by 65532:65532) and the chart mounts the data volume there with fsGroup 65532. The chart always starts myriarchd serve; the dev mode and the dev login are not available (setting config.dev or config.auth.devLogin fails the rendering).
  • config.server.tls.terminatedUpstream (default true) confirms to myriarchd that TLS ends at the ingress, Gateway or router (otherwise myriarchd does not start in production). secretEnv names must start with executors.secretEnvPrefix (MYRIARCH_EXEC_). config.auth.sessionTtl and sessionMaxAge limit sessions (2 h idle, 12 h absolute).
  • TLS: if config.server.publicUrl uses https, the Ingress needs ingress.tls (ingress.allowWithoutTls=true only when TLS terminates outside the ingress); an HTTPRoute needs httpRoute.tlsTerminatedAtGateway=true. The Route terminates TLS at the router and redirects plain HTTP requests (openshift.route.tls.insecureEdgeTerminationPolicy: Redirect); Allow serves plain HTTP at the router, and so ws://, and None serves none.
  • Metrics: port metrics (9090); serviceMonitor.enabled=true creates a ServiceMonitor, and prometheusRule.enabled=true a PrometheusRule with the alerts of the server (Alerting rules).
  • Test: helm test <release> checks /readyz and the OpAMP endpoint of the Service (Helm test).
  • Image: image.registry (default ghcr.io) and image.repository (default myriarch/myriarch) give the repository, and image.tag (default: appVersion) or, immutable, image.digest (sha256:<64 hex>; when set, the image is referenced as <repository>@<digest> and the tag is ignored) the version. Private registry: imagePullSecrets. See Image. Available from v0.2.0. image.registry and global.imageRegistry: the chart of v0.1.0 ignores them without a message, and its image.repository holds the registry host as well (ghcr.io/myriarch/myriarch).
  • Proxy and internal CA for outbound connections: extraEnv (HTTPS_PROXY, NO_PROXY) and extraVolumes / extraVolumeMounts for a CA ConfigMap, together with config.outbound.caFile and config.outbound.allowedCidrs.

Image

Available from v0.2.0. The chart of v0.1.0 has no image.registry and no global.imageRegistry and ignores them without a message; there the registry host is part of image.repository (default ghcr.io/myriarch/myriarch). With that chart, a repository without a host is pulled from the default registry of your container runtime.

The image is <registry>/<repository>:<tag>, or <registry>/<repository>@<digest> with image.digest:

ValueDefaultMeaning
image.registryghcr.ioRegistry host with an optional port. It applies when the repository has no registry host of its own. Empty means the default registry of the container runtime.
image.repositorymyriarch/myriarchRepository without the registry host.
image.tagemptyEmpty means the appVersion of the chart.
image.digestemptysha256: and 64 hexadecimal characters. It replaces the tag.
global.imageRegistrynot setReplaces image.registry, for example with the host of a mirror, also when a parent chart passes it.

A repository that starts with a registry host is used unchanged, and neither image.registry nor global.imageRegistry applies to it. The first path segment of the repository is a registry host when it contains a . or a : or is localhost. This is the form of the values of v0.1.0 (image.repository: registry.example.com/myriarch/myriarch), which therefore render the same image as before. A repository without a host now takes image.registry; if you pulled such an image through the default registry of your container runtime, set image.registry to the empty string or put the host into the repository.

# the image of a mirror, pinned by digest
helm install myriarch ... --set image.registry=registry.example.com:5000 --set image.digest=sha256:<64 hex>
# the v0.1.0 form still works
helm install myriarch ... --set image.repository=registry.example.com/myriarch/myriarch

Values schema

Available from v0.2.0. The chart of v0.1.0 has no schema and ignores a key that it does not know.

values.schema.json is a JSON Schema (draft 2020-12) that Helm reads for helm install, upgrade, template and lint. It is strict:

  • A key that the chart does not read is an error, also inside config. Before, Helm ignored such a key and the chart rendered without it, so a misspelled setting went unnoticed.
  • A value has the type that the chart needs: a number for a port, a string for a name, a list for a list, a boolean for a switch. Strings that name something have the syntax of the receiving system: a Kubernetes name, a quantity such as 512Mi, a registry host, a digest, a duration in Go syntax (30s, 1m30s; not 30 and not 30d). The type of a switch matters for the TLS checks: a template counts the string "false" as true, so ingress.allowWithoutTls: "false" used to switch the check of the Ingress off, and the schema refuses it.
  • The block config mirrors the configuration file of the server. It is generated from the configuration structure of the server (internal/config), with the same keys, types and descriptions as the configuration reference, so it cannot drift from the server.
  • Secrets are not values. The schema has no key for a database DSN, an OIDC client secret, a webhook secret, the URL of a notification channel or an enrollment token: use the matching *File settings and the Secret existingSecret. A declared enrollment token (enrollmentTokens[]) names a Secret or lets the chart generate one, and a Secret reference that has the form of an enrollment token (myre_...) is refused. The settings that the chart generates (config.auth.bootstrapAdmin, config.opamp.enrollmentTokens) and the development mode (config.dev, config.auth.devLogin) are not offered in config.
  • The few places that take a Kubernetes object as it is (extraVolumes, extraVolumeMounts, podSecurityContext, securityContext, topologySpreadConstraints, affinity, the Gateway API references) are open objects; the API server validates them.
  • A parent chart that includes this chart as a dependency can use it: global is open (the chart reads only global.imageRegistry), and the key enabled, which Helm reads for condition: myriarch.enabled, is accepted and ignored by the chart. Every other key that the parent passes under myriarch: is checked like a key of a standalone installation.
  • null removes a value that values.yaml defines, as in Helm. For a key that values.yaml does not define Helm keeps the null, and the schema accepts it only for the entries of the maps of the chart (annotations, labels, selectors), where the values files of examples/ use it to switch off an entry of an earlier file, and for global.imageRegistry. It refuses it everywhere else, also for the entries of the maps in config (executors.secrets, executors.secretEnvHosts, executors.secretFileHosts): the chart would render the null into the ConfigMap, and the server refuses it when it starts.

An error names the path of the value, for example at '/image/tagg': additional properties 'tagg' not allowed (the wording depends on the Helm version). The schema does not replace the checks of the templates, which relate several values (an Ingress needs trustedProxies and a source in the network policy) and which also hold when the validation is switched off with --skip-schema-validation; and it does not replace myriarchd config validate, which checks the meaning of the configuration (Verification).

values.schema.json is generated by make docs-gen from values.schema.source.yaml, which holds the hand-written part, and from internal/config; do not edit the JSON file. A new value of the chart goes into values.yaml, the templates and values.schema.source.yaml; the tests fail when values.yaml or a template uses a value that the schema does not list.

Values

Every value of the chart with its type, its default and its description. The table is generated from values.schema.json and values.yaml (make docs-gen), so it cannot drift from them. “unset” means that values.yaml has no entry and the chart applies its own default.

ValueTypeDefaultDescription
replicaCountinteger2Number of myriarchd replicas. All replicas serve agents and the API; the singleton jobs run on the leader.
image.registrystring"ghcr.io"Registry host, with an optional port, of the image, for example ghcr.io. It applies when image.repository has no registry host of its own, and global.imageRegistry takes precedence over it. Empty means the default registry of the container runtime.
image.repositorystring"myriarch/myriarch"Repository of the image without a registry host, for example myriarch/myriarch. A repository that starts with a registry host (registry.example.com/myriarch/myriarch, the form of the values of v0.1.0) is used unchanged and neither image.registry nor global.imageRegistry applies. The first segment is a registry host when it contains a . or a : or is localhost, as for the container runtimes; otherwise it belongs to the repository, which is in lower case.
image.tagstring""Image tag. Empty means the appVersion of the chart.
image.digeststring""Image digest, sha256: followed by 64 hexadecimal characters. When set, the image is referenced as repository@digest and the tag is ignored.
image.pullPolicyenum"IfNotPresent"Image pull policy.
imagePullSecretsarray[]Secrets for pulling the image from a private registry.
nameOverridestring""Overrides the chart name in resource names.
fullnameOverridestring""Overrides the complete resource name.
global.imageRegistrystring or nullunsetRegistry host, with an optional port, that replaces image.registry, for example the host of a mirror. A repository that has a registry host of its own is not affected. A parent chart may pass null for no mirror.
enabledbooleanunsetRead by the dependency condition of a parent chart (condition: myriarch.enabled), not by this chart, which ignores it. It has no default and no effect on a standalone installation.
serviceAccount.createbooleantrueCreate the ServiceAccount.
serviceAccount.namestring""Name of the ServiceAccount. Empty means a name generated from the release name.
serviceAccount.annotationsobject{}Annotations of the ServiceAccount.
configobjectThe configuration file of the server (myriarchd.yaml); keys, types and descriptions are those of the configuration reference. The chart defaults are in values.yaml.
oidcFromEnvironmentbooleanfalseDeclares that single sign-on is switched on by environment variables (MYRIARCH_AUTH_OIDC_ENABLED in extraEnv, a ConfigMap or a Secret), which the chart cannot read. The chart then treats OIDC as enabled; the server reads the environment.
bootstrapAdmin.enabledbooleantrueConfigure the bootstrap admin.
bootstrapAdmin.usernamestring"admin"User name of the account, 1 to 64 characters from letters, digits, “.”, “_”, “@” and “-”.
bootstrapAdmin.existingSecretstring""Secret that holds the credentials. Empty means that the chart generates the Secret <fullname>-bootstrap with a random one-time token.
bootstrapAdmin.tokenKeystring"token"Key of the one-time token in the Secret. Empty means no token, only a password hash, and needs existingSecret.
bootstrapAdmin.passwordHashKeystring""Key of a bcrypt password hash in existingSecret. Needs existingSecret.
bootstrapAdmin.keepWithOidcbooleanfalseKeep the bootstrap admin active next to single sign-on (break-glass).
enrollmentTokensarray[]Enrollment tokens for agents that exist from the first start. A token is never a value: each entry names a Secret that holds it, or the chart generates the Secret with a random token.
existingSecretstring"myriarch-secrets"Name of the Secret with the files of the server (database-dsn and the optional keys). It is mounted at /etc/myriarchd/secrets.
secretChecksum.enabledbooleantrueAdd the annotation checksum/secret to the pods.
secretEnvarray[]Environment variables for the executors, each taken from one key of the Secret existingSecret.
extraEnvarray[]Additional environment variables of the myriarchd container, in the format of a Kubernetes container.
extraVolumesarray[]Additional volumes of the pod, in the format of a Kubernetes pod.
extraVolumeMountsarray[]Additional volume mounts of the myriarchd container, in the format of a Kubernetes container.
openshift.enabledboolean or enumfalsetrue selects the OpenShift profile, false the standard profile, and auto the OpenShift profile when the cluster offers the API security.openshift.io/v1. helm template renders the standard profile for auto unless it gets –api-versions security.openshift.io/v1.
openshift.route.enabledbooleantrueCreate the Route in the OpenShift profile.
openshift.route.hoststring""Host of the Route. Empty means the host of config.server.publicUrl, without the port.
openshift.route.annotationsobject{}Further annotations of the Route. The router timeout annotations are always set unless this map sets them.
openshift.route.timeoutstring"3600s"Router timeout for the long-lived OpAMP WebSockets, a number with a unit (us, ms, s, m, h or d), for example 3600s.
openshift.route.tls.insecureEdgeTerminationPolicyenum"Redirect"What the router does with plain HTTP requests.
openshift.route.tls.externalCertificateSecretstring""Secret (kubernetes.io/tls, same namespace) with a certificate for the host of the Route. Empty means the default certificate of the router.
openshift.routerNamespaceSelector.matchLabelsobject{"policy-group.network.openshift.io/ingress":""}A map from strings to strings. An entry may be null: the values files of the examples switch off an entry of an earlier values file that way, and Helm keeps such a null for a key that values.yaml does not define.
openshift.routerNamespaceSelector.matchExpressionsarrayunset
openshift.dns.toarraysee values.yamlDestinations in NetworkPolicy peer format.
openshift.dns.portsarray[5353,53]Ports of the DNS service, used for UDP and TCP.
service.typeenum"ClusterIP"Service type.
service.portinteger8080Port of the HTTP service (UI, REST and OpAMP).
service.metricsPortinteger9090Port of the metrics endpoint.
service.annotationsobject{}Annotations of the Service.
networkPolicy.enabledbooleantrueRender the NetworkPolicy.
networkPolicy.ingress.fromarray[]Sources in NetworkPolicy peer format, for example the namespace of the ingress controller or Gateway. Required while ingress or httpRoute is enabled.
networkPolicy.ingress.allowSameNamespacebooleantrueAllow every pod of the release namespace to reach port 8080. It must be false while config.server.trustedProxies or config.opamp.clientCertHeader is set.
networkPolicy.metrics.fromarray[]Sources in NetworkPolicy peer format. Empty means nobody.
networkPolicy.egress.enabledbooleantrueRestrict egress to DNS and the ports below.
networkPolicy.egress.portsarray[5432,443]Allowed destination ports (TCP) for all destinations, by default 5432 for PostgreSQL and 443 for HTTPS. The Git SSH port is added automatically while config.gitops is enabled with an SSH URL.
networkPolicy.egress.extraPortsarray[]Further ports (TCP), merged into the rule above.
networkPolicy.egress.exceptCidrsarray["169.254.0.0/16"]Destinations that the rule excludes, by default the cloud metadata service.
networkPolicy.egress.dns.toarraysee values.yamlDestinations in NetworkPolicy peer format.
networkPolicy.egress.dns.portsarray[53]Ports of the DNS service, used for UDP and TCP.
networkPolicy.egress.extraRulesarray[]Further egress rules in NetworkPolicy format.
ingress.enabledbooleanfalseCreate an Ingress.
ingress.classNamestring""Ingress class. Empty means the default class of the cluster.
ingress.annotationsobject{}Annotations for the controller. OpAMP connections are long-lived WebSockets, so the controller needs long timeouts.
ingress.hostsarraysee values.yamlHost names and paths of the Ingress.
ingress.tlsarray[]TLS of the Ingress. Required while config.server.publicUrl is https, unless allowWithoutTls is set.
ingress.allowWithoutTlsbooleanfalseAllow an https publicUrl without ingress.tls, because TLS terminates outside the Ingress.
ingress.allowWithoutTrustedProxiesbooleanfalseAllow an empty config.server.trustedProxies, so that all clients appear with the address of the ingress controller.
httpRoute.enabledbooleanfalseCreate an HTTPRoute.
httpRoute.parentRefsarray[]Gateways that the route is bound to, in the format of the Gateway API. Required while enabled.
httpRoute.hostnamesarray[]Host names of the route. They must match config.server.publicUrl. Required while enabled.
httpRoute.annotationsobject{}Annotations of the HTTPRoute.
httpRoute.tlsTerminatedAtGatewaybooleanfalseConfirms that the Gateway listener terminates TLS. Required with an https publicUrl, because the chart cannot check the Gateway.
httpRoute.allowWithoutTrustedProxiesbooleanfalseAllow an empty config.server.trustedProxies.
httpRoute.timeouts.requeststringunsetA duration in the format of the Gateway API, for example 30s or 1h30m.
httpRoute.timeouts.backendRequeststringunsetA duration in the format of the Gateway API, for example 30s or 1h30m.
resources.requestsobject{"cpu":"250m","ephemeral-storage":"512Mi","memory":"512Mi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
resources.limitsobject{"ephemeral-storage":"5Gi","memory":"1Gi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
resources.claimsarrayunset
preStopSleepSecondsinteger10Pause before the shutdown, so that the endpoint disappears from the load balancers before the connections are closed.
terminationGracePeriodSecondsinteger60Grace period of the pod on termination.
probes.startup.pathstring"/healthz"HTTP path of the probe.
probes.startup.failureThresholdinteger30A number of seconds or attempts, at least 1.
probes.startup.periodSecondsinteger2A number of seconds or attempts, at least 1.
probes.liveness.pathstring"/healthz"HTTP path of the probe.
probes.liveness.periodSecondsinteger10A number of seconds or attempts, at least 1.
probes.liveness.timeoutSecondsinteger3A number of seconds or attempts, at least 1.
probes.liveness.failureThresholdinteger3A number of seconds or attempts, at least 1.
probes.readiness.pathstring"/readyz"HTTP path of the probe.
probes.readiness.periodSecondsinteger5A number of seconds or attempts, at least 1.
probes.readiness.timeoutSecondsinteger3A number of seconds or attempts, at least 1.
probes.readiness.failureThresholdinteger3A number of seconds or attempts, at least 1.
tests.enabledbooleantrueRender the test Pod. The Pod exists only while “helm test” runs. “helm template | kubectl apply” creates it as an ordinary Pod; “helm template –skip-tests” leaves it out.
tests.token.existingSecretstring""Name of the Secret with the token of the test. Empty means that the Pod mounts no Secret.
tests.token.keystring"token"Key of the token in the Secret.
tests.podAnnotationsobject{}Annotations of the test Pod, for example the opt-out from the sidecar injection of a service mesh. A sidecar that keeps running stops the Pod from completing. The chart sets the annotations of the hook itself, and a key that starts with helm.sh/hook is refused: it would change or, as a null, remove the hook, and the Pod would become an ordinary resource of the release.
tests.resources.requestsobject{"cpu":"10m","ephemeral-storage":"16Mi","memory":"32Mi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
tests.resources.limitsobject{"ephemeral-storage":"64Mi","memory":"64Mi"}Quantities by resource name, for example cpu, memory and ephemeral-storage.
podDisruptionBudget.enabledbooleantrueCreate the PodDisruptionBudget.
podDisruptionBudget.minAvailableinteger or string1Minimum number or percentage of available pods. Ignored while maxUnavailable is set.
podDisruptionBudget.maxUnavailableinteger or stringunsetMaximum number or percentage of unavailable pods. It replaces minAvailable when set.
podAnnotationsobject{}Annotations of the pods.
podLabelsobject{}Additional labels of the pods.
priorityClassNamestring""Priority class of the pods.
podSecurityContext.runAsNonRootbooleantrue
podSecurityContext.runAsUserinteger65532
podSecurityContext.runAsGroupinteger65532
podSecurityContext.fsGroupinteger65532
podSecurityContext.seccompProfile.typeenum"RuntimeDefault"
securityContext.allowPrivilegeEscalationbooleanfalse
securityContext.readOnlyRootFilesystembooleantrue
securityContext.privilegedbooleanunset
securityContext.runAsNonRootbooleanunset
securityContext.runAsUserintegerunset
securityContext.runAsGroupintegerunset
securityContext.capabilities.addarrayunset
securityContext.capabilities.droparray["ALL"]
securityContext.seccompProfile.typeenumunset
persistence.enabledbooleanfalseMount a PersistentVolumeClaim instead of an emptyDir.
persistence.existingClaimstring""Name of an existing claim. Empty means that the chart creates one.
persistence.storageClassstring""Storage class of the created claim. Empty means the default class.
persistence.accessModesarray["ReadWriteMany"]Access modes of the created claim.
persistence.sizenumber or string"10Gi"Size of the created claim.
emptyDir.sizeLimitnumber or string"4Gi"Size limit of the emptyDir volume. It must stay above config.artifacts.maxCacheBytes.
topologySpreadConstraintsarraysee values.yamlSpreads the replicas, in the format of a Kubernetes pod. The chart adds the label selector of the release to every entry.
nodeSelectorobject{}Node selector of the pods.
tolerationsarray[]Tolerations of the pods.
affinity.nodeAffinityobjectunset
affinity.podAffinityobjectunset
affinity.podAntiAffinityobjectunset
serviceMonitor.enabledbooleanfalseCreate the ServiceMonitor.
serviceMonitor.intervalstring"30s"Scrape interval, for example 30s.
serviceMonitor.labelsobject{}Labels of the ServiceMonitor, for example the label that selects it in the Prometheus resource.
prometheusRule.enabledbooleanfalseCreate the PrometheusRule.
prometheusRule.labelsobject{}Labels of the PrometheusRule, for example the label that selects it in the Prometheus resource (ruleSelector).

Misconfiguration messages

Available from v0.2.0. The schema, the first of the two layers. The chart of v0.1.0 has only the messages of the templates.

Two layers refuse a value, and both name it. The schema refuses what one value can get wrong (an unknown key, a wrong type, a malformed string) before it renders anything; Helm reports the path of the value as a JSON pointer. The templates refuse what depends on several values, and they repeat a few checks of the schema (the digest, the OpenShift mode, the names, the keys that the chart generates or does not offer) for the case that the validation is off. A template message is an execution error that starts with the value path, followed by what is wrong and how to fix it, for example config.server.trustedProxies: must not be empty when ingress.enabled is true; .... The complete list of paths:

PathCause
image.digestnot of the form sha256:<64 hex>
image.repositoryempty, for example set to null
config.server.trustedProxiesempty while ingress.enabled or httpRoute.enabled (unless allowWithoutTrustedProxies)
ingress.tlsempty with an https publicUrl (unless allowWithoutTls)
networkPolicy.ingress.fromempty while networkPolicy.enabled and ingress.enabled or httpRoute.enabled
networkPolicy.ingress.allowSameNamespacetrue while trustedProxies or opamp.clientCertHeader is set
httpRoute.parentRefsempty while httpRoute.enabled
httpRoute.hostnamesempty while httpRoute.enabled
httpRoute.tlsTerminatedAtGatewaynot true with an https publicUrl
openshift.enablednot true, false or auto
openshift.route.hostno host derivable from openshift.route.host or publicUrl
config.auth.bootstrapAdminset in config; use the top-level bootstrapAdmin values
config.opamp.enrollmentTokensset in config; use the top-level enrollmentTokens values
config.auth.devLoginnot available in this chart
config.devnot available in this chart
config.database.dsnset in config (the ConfigMap would hold the password); put the DSN into the Secret and use config.database.dsnFile
config.auth.oidc.clientSecretset in config; put it into the Secret and use config.auth.oidc.clientSecretFile
config.gitops.webhookSecretset in config; put it into the Secret and use config.gitops.webhookSecretFile
config.notifications.channels[<i>].urlset in config; put the URL into the Secret and set urlFile
bootstrapAdmin.enabledno login path: bootstrapAdmin.enabled is false while neither config.auth.oidc.enabled nor oidcFromEnvironment is true
bootstrapAdmin.tokenKeyexistingSecret set without tokenKey and passwordHashKey
bootstrapAdmin.passwordHashKeyset without existingSecret
bootstrapAdmin.usernamenot 1 to 64 characters of letters, digits, ., _, @, -
enrollmentTokens[<i>].namenot a DNS label, or duplicate
tests.token.keyempty while tests.token.existingSecret is set
tests.token.existingSecreta Secret of the server itself: existingSecret or the bootstrap Secret (generated, or bootstrapAdmin.existingSecret)
tests.podAnnotationsa key that starts with helm.sh/hook (the schema names the key; the template repeats the check)

Helm test

Available from v0.2.0. The chart of v0.1.0 has no test.

helm test myriarch -n myriarch

runs one Pod (templates/tests/test-connection.yaml, the Helm hook test) with the image of the server and the command myriarchd healthcheck against the Service of the release (http://<fullname>.<namespace>.svc:<service.port>):

  1. GET /readyz must answer 200: the database answers, the schema fits and the event bus is connected.
  2. A WebSocket upgrade at config.opamp.path must be refused with 401. That shows that the route and the authentication are in place, and the Pod needs no credential for it.

The test goes through the Service, so it checks one ready replica, not every replica. The command sends no OpAMP message, so it registers no agent and stores nothing (Server behaviour). The server counts the second request as an unauthorized OpAMP connect (myriarch_opamp_connect_rejected_total{reason="unauthorized"}), one for each run of the test; the alert MyriarchUnauthorizedConnects does not notice that. The Pod exists only while the test runs: a successful Pod is deleted, a failed one stays for kubectl -n myriarch logs <fullname>-test-connection until the next run. The output names the failing request, for example myriarchd: readyz failed: GET http://myriarch.myriarch.svc:8080/readyz answered 503, want 200.

The Pod follows the restricted Pod Security profile: non-root, no privilege escalation, all capabilities dropped, seccomp RuntimeDefault, a read-only root file system and no service account token. The OpenShift profile (openshift.enabled) leaves the user, the group and the fsGroup to the SCC, as for the server. It has no environment variable, mounts no Secret unless you opt in (below), uses image.*, imagePullSecrets, nodeSelector and tolerations of the server, and takes its requests and limits from tests.resources (default 10m CPU and 32Mi memory requested, 64Mi memory limit). tests.podAnnotations adds annotations, for example the opt-out from the sidecar injection of a service mesh (sidecar.istio.io/inject: "false"): a sidecar that keeps running stops the Pod from completing, and helm test then waits for it. The chart sets the annotations of the hook itself, so it refuses a key that starts with helm.sh/hook (a changed or null value would turn the Pod into an ordinary resource of the release). tests.enabled: false removes the Pod. helm template | kubectl apply and tools that render hooks as ordinary resources create it as a normal Pod that runs once and ends: use helm template --skip-tests or tests.enabled: false there. helm upgrade --reuse-values from a release of v0.1.0 renders with the values of that release in place of the defaults of the chart, and they have no tests and no prometheusRule: the chart then renders neither the Pod nor the PrometheusRule, as v0.1.0 did. Upgrade with --reset-then-reuse-values or a values file to get the defaults (Install with Helm).

Complete handshake with a token (opt-in)

With a token the test also proves that the server accepts an enrollment token: the upgrade must be answered with 101, and the command closes the connection at once without a message. Create a token for the test only (in the UI under Settings, Enrollment tokens, with a label of its own and a short expiry; never the token of your agents), put it into a Secret and name the Secret:

kubectl -n myriarch create secret generic myriarch-test-token --from-file=token=./test-token
helm upgrade myriarch ... --set tests.token.existingSecret=myriarch-test-token      # tests.token.key is token

The Pod mounts that one key, read-only, at /etc/myriarchd-test/token, and the command reads the token from there and nowhere else: it is never an argument or an environment variable and it is never printed. The chart refuses the Secrets of the server itself (existingSecret and the bootstrap Secret) as the Secret of the test, the schema refuses a name or key in the form of an enrollment token (myre_...), and the command refuses a file that holds anything but one enrollment token, so a Secret named by mistake is not sent to the server. Revoke the token when you no longer run the test.

Network access of the test

The NetworkPolicy selects the pods of the server, not the test Pod. For port 8080 of the server:

allowSameNamespacetrustedProxies or clientCertHeaderSource for the test Pod
true (default)not setnone needed: every pod of the namespace is a source
falsenot setthe chart adds one: the pods with the labels of the test Pod
false (required)setnone: the test cannot connect unless you list the test Pod yourself

The last row is deliberate. Those settings are safe only if no pod of the namespace reaches port 8080 directly (any pod could forge the forwarded client address or certificate), and every pod that carries the labels of the test Pod could use a rule for it. To run helm test there, accept that and name the labels in networkPolicy.ingress.from (the chart prints this hint with the install notes as well):

networkPolicy:
  ingress:
    allowSameNamespace: false
    from:
      - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: <ingress-namespace>}}
      - podSelector:
          matchLabels:
            app.kubernetes.io/component: test
            app.kubernetes.io/instance: myriarch          # the release name
            app.kubernetes.io/name: myriarch-test         # the name of the chart, or nameOverride, and -test

A cluster with a default-deny policy for the namespace (an egress rule for the test Pod, DNS) needs rules of its own for the test Pod as well.

tests.enabled: false removes the Pod and the rule: the NetworkPolicy is then the one of v0.1.0.

When the test fails

Output of the PodCause
readyz failed: cannot reach ...: no answer within 10s, or connection refusedNo ready replica, or the NetworkPolicy keeps the test Pod out (Network access of the test), or the Service name does not resolve in this cluster
readyz failed: ... answered 503, want 200: the server is not readyThe server reports that its database, schema or event bus is not ready; its log names the cause
opamp failed: the upgrade at /v1/opamp was answered with 404config.opamp.path differs from the path that the server serves, for example an environment override in extraEnv
opamp failed: ... was refused with 401 although a token was givenThe token of the test is revoked, expired or unknown, or the server also requires a client certificate
--token-file: file ... must contain an enrollment tokenThe key of tests.token.key holds something else (the Pod then makes no request)

Alerting rules

Available from v0.2.0.

prometheusRule.enabled=true creates a Prometheus Operator PrometheusRule (CRD monitoring.coreos.com/v1; the cluster needs the CRD, otherwise helm install fails) with the alerts of the server. prometheusRule.labels adds labels, for example the one that the ruleSelector of the Prometheus resource uses (release: kube-prometheus-stack). There is no dashboard.

GroupAlertSeverityFires when
availabilityMyriarchNoLeadercriticalno replica holds the leader lock for 3 minutes
availabilityMyriarchMultipleLeaderscriticalmore than one replica reports the leader lock for 3 minutes
availabilityMyriarchReplicaNotReadywarningfewer than replicaCount pods are available for 10 minutes (kube-state-metrics)
availabilityMyriarchTargetDownwarningthe metrics of a replica are unreachable for 5 minutes (networkPolicy.metrics.from)
agentsMyriarchAgentConnectionsDroppedwarningthe OpAMP connections fall by more than 20 % within 15 minutes
agentsMyriarchConnectRejectedwarningagents are rejected with 429 or 503 (connection limits) for 10 minutes
agentsMyriarchUnauthorizedConnectsinfomore than one unauthorized OpAMP connect per second for 15 minutes
jobsMyriarchReleaseTrackerErrorswarningthe release tracker fails twice within 3 hours
jobsMyriarchGitOpsSyncFailingwarningthe GitOps repository is invalid or unreachable for 15 minutes
jobsMyriarchGitOpsStalewarningno successful GitOps sync for 30 minutes
jobsMyriarchRolloutFailedwarninga rollout moved to failed
jobsMyriarchNotificationsFailinginfoa notification channel fails for 10 minutes
jobsMyriarchHousekeepingFailinginfohousekeeping runs fail for 10 minutes

The rules select the series of the server by job and namespace. The job label is the name of the Service, because the ServiceMonitor of this chart (serviceMonitor.enabled=true) sets no jobLabel; with another scrape configuration use the example for clusters without Helm, which has the same rules for the release myriarch in the namespace myriarch, and adapt the selectors. ci/helm-conform.sh fails when the two differ. Details of the metrics: Metrics.

Production example

A complete, neutral example (HA values with OIDC, gateway collector overlay, secrets script and an offline verification script) lives in deploy/examples/production/; see its README. The overlays in deploy/helm/myriarch/examples/ (ingress-*.yaml, openshift.yaml, quickstart-dex.yaml, dex/dex.yaml, install-guide.yaml) are not part of the packaged chart. install-guide.yaml is the values file of the guide Install with Helm: minimal installation, GitOps over HTTPS, an HTTPRoute and one declared enrollment token.

Verification

helm lint deploy/helm/myriarch
helm template myriarch deploy/helm/myriarch -f my-values.yaml | kubectl apply --dry-run=server -f -
# validate the rendered configuration without starting myriarchd (files and secrets do not have to exist):
helm template myriarch deploy/helm/myriarch -f my-values.yaml | yq 'select(.kind == "ConfigMap").data["myriarchd.yaml"]' > myriarchd.yaml
myriarchd config validate --config myriarchd.yaml --skip-files
# render the OpenShift profile without a cluster:
helm template myriarch deploy/helm/myriarch --set openshift.enabled=true
helm template myriarch deploy/helm/myriarch --set openshift.enabled=auto --api-versions security.openshift.io/v1
# everything together for the production example (lint, render, config validate, kustomize, kubeconform):
deploy/examples/production/verify.sh
# CI: both profiles, all examples and the assertions of the chart contract:
ci/helm-conform.sh
# after the installation: /readyz and the OpAMP endpoint through the Service (see "Helm test")
helm test myriarch -n myriarch