swarm UI: an alerts page, read by the controller with a Viewer Grafana token from bao
swarm-bao-grafana-controller-token, on the host that runs Grafana, logs in to Grafana's API over its socket as the built-in admin (the password swarm-bao-grafana-admin minted, #5138) and keeps a `swarm-controller` service account that is an enabled Viewer. It mints a token for that account and writes it to bao at swarm/controller/swarm-controller/grafana/token under Grafana's leaf. A ten-minute timer runs it again. The unit keeps a stored token Grafana accepts. If Grafana refuses the stored token with 401, or bao holds none, the unit deletes every token of the account and mints a new one. Grafana's grant gains create/read/update on that leaf, and the controller's gains read. swarm-controller serves GET /api/alerts from Grafana's /api/prometheus/grafana/api/v1/alerts. It reads the token from bao, keeps it in memory, and re-reads bao once on a 401. The HTTP client has 5s connect and 15s request timeouts. Any failure is a 503 that names its cause. swarm-ui gets an /alerts page listing them, built the same way as the hives page. This is option (c) on #5129, the operator's choice.
This commit is contained in:
parent
8cb6273782
commit
7d09776742
23 changed files with 888 additions and 37 deletions
|
|
@ -67,7 +67,8 @@ let
|
|||
];
|
||||
};
|
||||
# ./swarm-grafana.nix, under Grafana's leaf, which may create the admin
|
||||
# password and the metrics bearer only once that policy is written.
|
||||
# password, the metrics bearer and the controller's Grafana token only
|
||||
# once that policy is written.
|
||||
swarm-bao-grafana-admin = {
|
||||
here = deployCfg.grafana.enable;
|
||||
policies = [ "swarm-bao-grafana-oidc-policy" ];
|
||||
|
|
@ -76,6 +77,10 @@ let
|
|||
here = deployCfg.grafana.enable;
|
||||
policies = [ "swarm-bao-grafana-oidc-policy" ];
|
||||
};
|
||||
swarm-bao-grafana-controller-token = {
|
||||
here = deployCfg.grafana.enable;
|
||||
policies = [ "swarm-bao-grafana-oidc-policy" ];
|
||||
};
|
||||
# ./hive-matrix.nix, under matrix-ctl's leaf.
|
||||
swarm-bao-matrix-oidc = {
|
||||
here = deployCfg.matrix.enable;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ let
|
|||
swarm-bao-grafana-oidc = deployCfg.grafana.enable;
|
||||
swarm-bao-grafana-admin = deployCfg.grafana.enable;
|
||||
swarm-bao-grafana-metrics-bearer = deployCfg.grafana.enable;
|
||||
swarm-bao-grafana-controller-token = deployCfg.grafana.enable;
|
||||
# ./swarm-nats.nix
|
||||
swarm-bao-nats-callout-xkey = deployCfg.nats.enable;
|
||||
# ./swarm-ci-runner.nix
|
||||
|
|
|
|||
|
|
@ -369,9 +369,9 @@ let
|
|||
# Each hive's matrix sender token (`matrix_account::hive_sender`) grants `read`
|
||||
# for the same keep-if-live reason, and `+` for the same glob-only-as-last-segment reason.
|
||||
#
|
||||
# Three leaves it reads and never writes: the swarm appservice token
|
||||
# (matrix-ctl, `matrixCtlPolicyText` below), its own OIDC client secret (the
|
||||
# publisher) and its own forge token (the forge's host, `forgePublisherWriters`).
|
||||
# Four leaves it reads and never writes: the swarm appservice token (matrix-ctl),
|
||||
# its OIDC client secret (the publisher), its forge token (the forge's host)
|
||||
# and its Grafana token (Grafana's host). Each writer's grant is named below.
|
||||
#
|
||||
# The agent PKI grant is its only path on that mount: it can ask the one role
|
||||
# for a certificate, not write that role or reach the issuer.
|
||||
|
|
@ -457,6 +457,10 @@ let
|
|||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "${credentialMountPath}/data/${controllerGrafanaTokenLeaf}" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "${agentPkiMountPath}/issue/${agentPkiRoleName}" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
|
@ -582,6 +586,11 @@ let
|
|||
# it; the controller reads it and holds no other copy.
|
||||
controllerForgeTokenLeaf = "swarm/controller/swarm-controller/forge/token";
|
||||
|
||||
# The controller's Viewer token for Grafana's API, the nix half of
|
||||
# `swarm_secret_client::grafana::controller_token_path`. Grafana's host
|
||||
# writes it; the controller reads it and holds no other copy.
|
||||
controllerGrafanaTokenLeaf = "swarm/controller/swarm-controller/grafana/token";
|
||||
|
||||
# The forge's runner registration token, the nix half of
|
||||
# `swarm_secret_client::ci::registration_token_path`. The controller writes
|
||||
# it and cannot read it back (`forge::runner_token` keeps the value it last
|
||||
|
|
@ -911,6 +920,10 @@ let
|
|||
# in ./swarm-grafana.nix) and the bearer its metrics datasource presents
|
||||
# (`swarm-bao-grafana-metrics-bearer`). `create` without `update`, so a stored
|
||||
# value is never replaced.
|
||||
#
|
||||
# The controller's Grafana token is the one it may replace:
|
||||
# `swarm-bao-grafana-controller-token` mints a new one when Grafana refuses
|
||||
# the stored one, as it does after its database is recreated.
|
||||
grafanaOidcReaders = [
|
||||
{
|
||||
name = "swarm-grafana-oidc";
|
||||
|
|
@ -918,7 +931,12 @@ let
|
|||
policyText =
|
||||
readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.grafana.oidc.clientId}/oidc/client"
|
||||
+ createStanza "${credentialMountPath}/data/${grafanaAdminLeaf}"
|
||||
+ createStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}";
|
||||
+ createStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}"
|
||||
+ ''
|
||||
path "${credentialMountPath}/data/${controllerGrafanaTokenLeaf}" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -811,6 +811,11 @@ in
|
|||
# separated: a matrix user id cannot contain a space. Consumed by
|
||||
# swarm-controller/src/matrix_account/operator_admin.rs.
|
||||
SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS = lib.concatStringsSep " " matrixCfg.adminSync.ignoreUsers;
|
||||
# Where `GET /api/alerts` reads alerts (swarm-controller/src/alerts.rs).
|
||||
# Every swarm has one Grafana, reached by name wherever it runs. The
|
||||
# token is in the store, written by `swarm-bao-grafana-controller-token`
|
||||
# on Grafana's host.
|
||||
SWARM_CONTROLLER_GRAFANA_URL = "https://${swarmCfg.grafana.domain}";
|
||||
}
|
||||
// queueEnv
|
||||
// forgeEnv
|
||||
|
|
|
|||
|
|
@ -273,6 +273,14 @@ let
|
|||
metricsBearerPath = "/var/lib/grafana-metrics/bearer";
|
||||
hostMetricsBearerPath = "/var/lib/nixos-containers/${cfg.machine}${metricsBearerPath}";
|
||||
|
||||
# swarm-controller's token for Grafana's API, minted by
|
||||
# `swarm-bao-grafana-controller-token` below for a Viewer service account of
|
||||
# this name. `controllerGrafanaTokenLeaf` in ./swarm-bao.nix is the nix half
|
||||
# of the path, `swarm_secret_client::grafana::controller_token_path` the Rust
|
||||
# half.
|
||||
controllerAccountName = "swarm-controller";
|
||||
controllerTokenStorePath = "secret/swarm/controller/swarm-controller/grafana/token";
|
||||
|
||||
socketPath = "${deployCfg.grafana.socketDir}/grafana.sock";
|
||||
|
||||
# Both static NixOS ids, and both checked rather than assumed: nginx has
|
||||
|
|
@ -849,6 +857,148 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# swarm-controller reads Grafana's alerts with a token of a Viewer service
|
||||
# account, created here through Grafana's API as the built-in admin and
|
||||
# handed over through the store.
|
||||
#
|
||||
# Re-run by the timer below. A stored token Grafana still accepts is kept;
|
||||
# one it refuses with 401, or none at all, is replaced: every token of the
|
||||
# account is deleted and one new one is minted and stored. The account's
|
||||
# role is set back to Viewer on every run.
|
||||
systemd.services.swarm-bao-grafana-controller-token = {
|
||||
description = "give swarm-controller a Viewer token for Grafana's API through the swarm secret store";
|
||||
# Ordered after the store where it runs here:
|
||||
# ./glue-bao-readers-store-order.nix.
|
||||
after = [
|
||||
"container@${cfg.machine}.service"
|
||||
"swarm-bao-grafana-admin.service"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
baoDeploy.package
|
||||
pkgs.coreutils
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
];
|
||||
# ./lib/store-retry.nix. A Grafana still starting refuses the
|
||||
# connection, and that attempt fails into the same retry.
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
# No `RemainAfterExit`: the timer below starts this again.
|
||||
Type = "oneshot";
|
||||
SyslogIdentifier = "swarm-bao-grafana-controller-token";
|
||||
# Grafana's answers, the new token among them, pass through a temp file.
|
||||
PrivateTmp = true;
|
||||
# Each call below is bounded by curl's own timeouts; a run makes up to
|
||||
# about ten of them.
|
||||
TimeoutStartSec = "3min";
|
||||
};
|
||||
environment = {
|
||||
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
||||
BAO_CLIENT_CERT = baoDeploy.grafanaOidcClientCertFile;
|
||||
BAO_CLIENT_KEY = baoDeploy.grafanaOidcClientKeyFile;
|
||||
}
|
||||
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
|
||||
BAO_CACERT = baoDeploy.serverCaFile;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
err="$(mktemp)"
|
||||
body="$(mktemp)"
|
||||
trap 'rm -f "$err" "$body"' EXIT
|
||||
|
||||
# One call to Grafana's API through its socket. Prints the HTTP
|
||||
# status; the body lands in "$body". The credential reaches curl as
|
||||
# a header file, never as an argument.
|
||||
grafana() {
|
||||
local auth="$1" method="$2" path="$3"
|
||||
shift 3
|
||||
curl --silent --show-error --connect-timeout 5 --max-time 15 \
|
||||
--unix-socket ${lib.escapeShellArg socketPath} \
|
||||
-H @<(printf 'Authorization: %s\n' "$auth") \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X "$method" -o "$body" -w '%{http_code}' "$@" \
|
||||
"http://localhost$path"
|
||||
}
|
||||
|
||||
# Fails the attempt unless the last call answered one of the statuses
|
||||
# given.
|
||||
expect() {
|
||||
local what="$1" status="$2"
|
||||
shift 2
|
||||
for ok in "$@"; do
|
||||
[ "$status" = "$ok" ] && return 0
|
||||
done
|
||||
echo "Grafana answered $status to $what:" >&2
|
||||
cat "$body" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ ! -s ${lib.escapeShellArg hostAdminPasswordPath} ]; then
|
||||
echo "no Grafana admin password at ${hostAdminPasswordPath} yet; swarm-bao-grafana-admin writes it." >&2
|
||||
exit 1
|
||||
fi
|
||||
admin="Basic $(printf 'admin:%s' "$(< ${lib.escapeShellArg hostAdminPasswordPath})" | base64 -w0)"
|
||||
|
||||
status="$(grafana "$admin" GET '/api/serviceaccounts/search?perpage=1000&query=${controllerAccountName}')"
|
||||
expect "the service account search" "$status" 200
|
||||
account="$(jq -c --arg n ${controllerAccountName} '[.serviceAccounts[] | select(.name == $n)][0] // empty' "$body")"
|
||||
if [ -z "$account" ]; then
|
||||
status="$(grafana "$admin" POST /api/serviceaccounts \
|
||||
--data '{"name":"${controllerAccountName}","role":"Viewer","isDisabled":false}')"
|
||||
expect "creating the ${controllerAccountName} service account" "$status" 200 201
|
||||
account="$(jq -c . "$body")"
|
||||
echo "created the ${controllerAccountName} service account as a Viewer"
|
||||
fi
|
||||
id="$(jq -r .id <<<"$account")"
|
||||
if [ "$(jq -r '"\(.role) \(.isDisabled // false)"' <<<"$account")" != "Viewer false" ]; then
|
||||
status="$(grafana "$admin" PATCH "/api/serviceaccounts/$id" \
|
||||
--data '{"role":"Viewer","isDisabled":false}')"
|
||||
expect "setting the ${controllerAccountName} service account to an enabled Viewer" "$status" 200
|
||||
echo "set the ${controllerAccountName} service account back to an enabled Viewer"
|
||||
fi
|
||||
|
||||
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
||||
echo "could not log in to swarm-bao with Grafana's certificate; leaving swarm-controller's Grafana token as it is." >&2
|
||||
cat "$err" >&2
|
||||
exit 1
|
||||
fi
|
||||
export BAO_TOKEN
|
||||
|
||||
# Checked against the call the controller makes with it.
|
||||
if stored="$(bao kv get -field=value ${lib.escapeShellArg controllerTokenStorePath} 2>"$err")" && [ -n "$stored" ]; then
|
||||
status="$(grafana "Bearer $stored" GET /api/prometheus/grafana/api/v1/alerts)"
|
||||
case "$status" in
|
||||
200) exit 0 ;;
|
||||
401) echo "Grafana refuses the stored token for ${controllerAccountName}; replacing it." ;;
|
||||
*) expect "reading alerts with the stored token" "$status" 200 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
status="$(grafana "$admin" GET "/api/serviceaccounts/$id/tokens")"
|
||||
expect "listing the ${controllerAccountName} tokens" "$status" 200
|
||||
for token_id in $(jq -r '.[].id' "$body"); do
|
||||
status="$(grafana "$admin" DELETE "/api/serviceaccounts/$id/tokens/$token_id")"
|
||||
expect "deleting token $token_id of ${controllerAccountName}" "$status" 200
|
||||
done
|
||||
|
||||
status="$(grafana "$admin" POST "/api/serviceaccounts/$id/tokens" \
|
||||
--data "{\"name\":\"${controllerAccountName}-$(date +%s)\"}")"
|
||||
expect "minting a ${controllerAccountName} token" "$status" 200
|
||||
key="$(jq -r .key "$body")"
|
||||
: >"$body"
|
||||
if ! printf '%s' "$key" | bao kv put ${lib.escapeShellArg controllerTokenStorePath} value=- >/dev/null 2>"$err"; then
|
||||
echo "could not store the new ${controllerAccountName} token at ${controllerTokenStorePath}; the next run replaces it." >&2
|
||||
cat "$err" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "stored a new ${controllerAccountName} token at ${controllerTokenStorePath}"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.swarm-bao-grafana-controller-token = storeRefreshTimer "check swarm-controller's Grafana token and replace it if Grafana refuses it";
|
||||
|
||||
containers.${cfg.machine} = {
|
||||
autoStart = true;
|
||||
ephemeral = false;
|
||||
|
|
|
|||
|
|
@ -221,10 +221,10 @@ let
|
|||
&& builtins.elem "bao-client-key.pem:/var/lib/swarm-bao-pki/controller-key.pem" s.swarm-controller.serviceConfig.LoadCredential;
|
||||
}
|
||||
{
|
||||
# The forge token comes from the store, so the unit is handed no file for
|
||||
# it, and neither fixture runs the forge: a controller reaches it by name
|
||||
# wherever it runs.
|
||||
name = "the controller is handed no forge token file, and is pointed at the forge away from it";
|
||||
# The forge and Grafana tokens come from the store, so the unit is handed
|
||||
# no file for either, and neither fixture runs the forge or Grafana: a
|
||||
# controller reaches both by name wherever they run.
|
||||
name = "the controller is handed no forge token file, and is pointed at the forge and Grafana away from it";
|
||||
ok =
|
||||
lib.all
|
||||
(
|
||||
|
|
@ -235,6 +235,7 @@ let
|
|||
!(lib.any (c: lib.hasPrefix "forge-token:" c) u.serviceConfig.LoadCredential)
|
||||
&& !(u.environment ? SWARM_CONTROLLER_FORGE_TOKEN_FILE)
|
||||
&& u.environment.SWARM_CONTROLLER_FORGE_URL == "https://forge.t.local"
|
||||
&& u.environment.SWARM_CONTROLLER_GRAFANA_URL == "https://grafana.t.local"
|
||||
)
|
||||
[
|
||||
baoControllerHere
|
||||
|
|
|
|||
|
|
@ -597,8 +597,10 @@ let
|
|||
#
|
||||
# The admin password and the metrics bearer are the leaves it may create,
|
||||
# and without `update`: a grant that could replace one would let a second
|
||||
# run rotate a value Grafana or the metrics store already holds.
|
||||
name = "the Grafana reader reads its own client secret and creates its admin password and metrics bearer, and nothing else";
|
||||
# run rotate a value Grafana or the metrics store already holds. The
|
||||
# controller's Grafana token is the one leaf it may replace, which it
|
||||
# does when Grafana refuses the stored token.
|
||||
name = "the Grafana reader reads its own client secret, creates its admin password and metrics bearer, and writes the controller's Grafana token, and nothing else";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-grafana-oidc-policy.script;
|
||||
|
|
@ -622,6 +624,14 @@ let
|
|||
"read"
|
||||
];
|
||||
}
|
||||
{
|
||||
path = "secret/data/swarm/controller/swarm-controller/grafana/token";
|
||||
caps = [
|
||||
"create"
|
||||
"read"
|
||||
"update"
|
||||
];
|
||||
}
|
||||
]
|
||||
&& !(lib.hasInfix "secret/data/swarm/agents" s)
|
||||
&& !(lib.hasInfix "secret/data/swarm/hives" s)
|
||||
|
|
@ -938,18 +948,27 @@ let
|
|||
&& !(noReaders ? swarm-bao-nats-callout-xkey);
|
||||
}
|
||||
{
|
||||
# The admin password is created under Grafana's leaf, so its unit waits
|
||||
# for the unit writing that leaf's role and policy; before it, the create
|
||||
# is refused. The second arm is the absence control.
|
||||
name = "grafana's admin password unit is ordered after grafana's policy unit";
|
||||
# The admin password and the controller's Grafana token are written under
|
||||
# Grafana's leaf, so their units wait for the unit writing that leaf's
|
||||
# role and policy; before it, the write is refused. The last arm is the
|
||||
# absence control.
|
||||
name = "grafana's admin password and controller token units are ordered after grafana's policy unit";
|
||||
ok =
|
||||
let
|
||||
u = baoGrantWithConsumers.systemd.services.swarm-bao-grafana-admin;
|
||||
in
|
||||
lib.elem "swarm-bao-grafana-oidc-policy.service" u.after
|
||||
&& lib.elem "swarm-bao-grafana-oidc-policy.service" u.wants
|
||||
&& !(lib.elem "swarm-bao-grafana-oidc-policy.service" u.requires)
|
||||
&& !(baoGrantNoReaders.systemd.services ? swarm-bao-grafana-admin);
|
||||
lib.all
|
||||
(
|
||||
unit:
|
||||
let
|
||||
u = baoGrantWithConsumers.systemd.services.${unit};
|
||||
in
|
||||
lib.elem "swarm-bao-grafana-oidc-policy.service" u.after
|
||||
&& lib.elem "swarm-bao-grafana-oidc-policy.service" u.wants
|
||||
&& !(lib.elem "swarm-bao-grafana-oidc-policy.service" u.requires)
|
||||
&& !(baoGrantNoReaders.systemd.services ? ${unit})
|
||||
)
|
||||
[
|
||||
"swarm-bao-grafana-admin"
|
||||
"swarm-bao-grafana-controller-token"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The ordering is set apart from each reader's own definition, so it can
|
||||
|
|
@ -1100,6 +1119,7 @@ let
|
|||
"swarm-bao-forwarder-oidc"
|
||||
"swarm-bao-grafana-admin"
|
||||
"swarm-bao-grafana-metrics-bearer"
|
||||
"swarm-bao-grafana-controller-token"
|
||||
]
|
||||
++ policyReaders
|
||||
);
|
||||
|
|
@ -1715,6 +1735,25 @@ let
|
|||
"swarm-bao-forge-publisher-policy"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The controller's Grafana token: read on the one leaf, pinned as the
|
||||
# whole stanza so an added write fails, and named by no policy but the
|
||||
# controller's and Grafana's, which writes it.
|
||||
name = "the controller reads its Grafana token, and only Grafana's grant writes it";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
naming = lib.filter (
|
||||
u: lib.hasInfix "swarm-controller/grafana/token" baoGrantHere.systemd.services.${u}.script
|
||||
) grantingUnitNames;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/grafana/token\" {\n capabilities = [\"read\"]\n}" s
|
||||
&&
|
||||
lib.sort lib.lessThan naming == [
|
||||
"swarm-bao-controller-policy"
|
||||
"swarm-bao-grafana-oidc-policy"
|
||||
];
|
||||
}
|
||||
{
|
||||
# The forge's OIDC client secret by name: the forge publisher reads it,
|
||||
# and the secret publisher writes it under its `swarm/services/*` grant,
|
||||
|
|
|
|||
|
|
@ -249,6 +249,33 @@ let
|
|||
&& lib.hasInfix "admin reset-admin-password --password-from-stdin < /var/lib/grafana-admin/password" service.preStart
|
||||
&& lib.hasInfix "-config ${resetConfig}" service.script;
|
||||
}
|
||||
{
|
||||
# swarm-controller reads alerts with this token, so the account it
|
||||
# belongs to is created and kept a Viewer, the token lands at the path
|
||||
# the controller's grant reads, and an attempt against a Grafana or a
|
||||
# store that never answers ends and retries. The timer re-runs it, so
|
||||
# the unit must not stay active.
|
||||
name = "the controller's Grafana token is a Viewer account's, stored under Grafana's leaf, and each attempt is bounded";
|
||||
ok =
|
||||
let
|
||||
unit = grafanaRemoteAuthelia.systemd.services.swarm-bao-grafana-controller-token;
|
||||
s = unit.script;
|
||||
in
|
||||
!(lib.elem (toString (unit.serviceConfig.TimeoutStartSec or "infinity")) [
|
||||
"infinity"
|
||||
"0"
|
||||
])
|
||||
&& lib.hasInfix "curl --silent --show-error --connect-timeout 5 --max-time 15" s
|
||||
&& lib.hasInfix "--data '{\"name\":\"swarm-controller\",\"role\":\"Viewer\",\"isDisabled\":false}'" s
|
||||
&& lib.hasInfix "--data '{\"role\":\"Viewer\",\"isDisabled\":false}'" s
|
||||
&& lib.hasInfix "bao kv put secret/swarm/controller/swarm-controller/grafana/token value=-" s
|
||||
&& lib.hasInfix "--unix-socket /run/swarm-grafana/grafana.sock" s
|
||||
&& unit.environment.BAO_CLIENT_CERT == "/etc/pki/bao-grafana-oidc.pem"
|
||||
&& unit.serviceConfig.Restart == "on-failure"
|
||||
&& !(unit.serviceConfig.RemainAfterExit or false)
|
||||
&& lib.elem "swarm-bao-grafana-admin.service" unit.after
|
||||
&& grafanaRemoteAuthelia.systemd.timers ? swarm-bao-grafana-controller-token;
|
||||
}
|
||||
{
|
||||
# Same 403-not-a-miss reason as the matrix and queue arms below: the
|
||||
# reader's grant covers the `services` prefix, so a path outside it is
|
||||
|
|
|
|||
Loading…
Reference in a new issue