Start working on docserver

This commit is contained in:
Daniel Berteaud 2023-11-08 22:40:57 +01:00
parent f22f062edd
commit 604e6e6215
24 changed files with 762 additions and 0 deletions

4
bundles.yml Normal file
View File

@ -0,0 +1,4 @@
---
dependencies:
- url: ../common.git

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "[[ .oo.instance ]][[ .consul.suffix ]]"
Protocol = "http"

View File

@ -0,0 +1,15 @@
Kind = "service-intentions"
Name = "[[ .oo.instance ]][[ .consul.suffix ]]"
Sources = [
{
Name = "[[ .traefik.instance ]]"
Permissions = [
{
Action = "allow"
HTTP {
PathPrefix = "/"
}
}
]
}
]

View File

@ -0,0 +1,29 @@
FROM [[ .docker.repo ]][[ .docker.base_images.alma9.image ]]
MAINTAINER [[ .docker.maintainer ]]
ENV LANG=[[ .locale.lang ]] \
TZ=[[ .locale.tz ]]
USER root
COPY root/etc/yum.repos.d/ /etc/yum.repos.d/
RUN set -euxo pipefail &&\
dnf -y update &&\
rpm -i https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm &&\
dnf -y module disable postgresql &&\
dnf -y install \
postgresql16 \
onlyoffice-documentserver \
supervisor \
nginx \
jq \
&&\
chown :ds /etc/onlyoffice/documentserver/* &&\
mkdir -p /data &&\
chown ds:ds /data
COPY root/ /
EXPOSE 8084
USER ds
CMD ["supervisord", "-n", "-c", "/etc/supervisord.conf"]

View File

@ -0,0 +1,5 @@
#!/bin/sh
set -euo pipefail
mkdir -p /data/files /data/wopi /data/templates

View File

@ -0,0 +1,53 @@
#!/bin/sh
set -euo pipefail
function get_value(){
local KEY=$1
local VALUE=$(jq -r -s \
" .[0].services.CoAuthoring.sql * .[1].services.CoAuthoring.sql | .${KEY}" \
/etc/onlyoffice/documentserver/default.json \
/etc/onlyoffice/documentserver/production-linux.json)
echo ${VALUE}
}
export PGHOST=$(get_value "dbHost")
export DGPORT=$(get_value "dbport")
export PGDATABASE=$(get_value "dbName")
export PGUSER=$(get_value "dbUser")
export PGPASSWORD=$(get_value "dbPass")
function init_db(){
psql -f /var/www/onlyoffice/documentserver/server/schema/postgresql/createdb.sql
}
function reset_db(){
psql -f /var/www/onlyoffice/documentserver/server/schema/postgresql/removetbl.sql
}
function write_version(){
echo $(rpm -q onlyoffice-documentserver --qf "%{VERSION}-%{RELEASE}") > /data/onlyoffice.version
}
export PGHOST=$(get_value "dbHost")
export DGPORT=$(get_value "dbport")
export PGDATABASE=$(get_value "dbName")
export PGUSER=$(get_value "dbUser")
export PGPASSWORD=$(get_value "dbPass")
if [ ! -e "/data/onlyoffice.version" ]; then
echo "File /data/onlyoffice.version doesn't exist, initializing database"
init_db
write_version
else
PREVIOUS_VERSION=$(head -n 1 /data/onlyoffice.version)
CURRENT_VERSION=$(rpm -q onlyoffice-documentserver --qf "%{VERSION}-%{RELEASE}")
if [ "${CURRENT_VERSION}" != "${PREVIOUS_VERSION}" ]; then
echo "Running ${CURRENT_VERSION} while previous version was ${PREVIOUS_VERSION}. Reinitializing database"
reset_db
init_db
write_version
else
echo "Running ${CURRENT_VERSION}, same as previous version"
fi
fi

View File

@ -0,0 +1,48 @@
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_temp_path /tmp/proxy_temp;
client_body_temp_path /tmp/client_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
keepalive_timeout 65;
include /etc/onlyoffice/documentserver/nginx/includes/http-common.conf;
server {
listen 0.0.0.0:8084;
server_name _;
client_max_body_size 100m;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
# Override the fonts endpoint to add a Content-Type so reverse proxies can handle caching corectly
location ~ ^(\/[\d]+\.[\d]+\.[\d]+[\.|-][\d]+)?\/fonts(\/.*)$ {
add_header Content-Type font/opentype;
expires 365d;
alias /var/www/onlyoffice/documentserver/fonts$2;
}
set $secure_link_secret verysecretstring;
include /etc/onlyoffice/documentserver/nginx/includes/ds-docservice.conf;
}
}

View File

@ -0,0 +1,74 @@
{
"log": {
"filePath": "/etc/onlyoffice/documentserver/log4js/production.json"
},
"storage": {
"fs": {
"folderPath": "/data/files"
}
},
"wopi": {
"htmlTemplate" : "/data/wopi"
},
"services": {
"CoAuthoring": {
"server": {
"newFileTemplate" : "/data/templates",
"static_content": {
"/fonts": {
"path": "/var/www/onlyoffice/documentserver/fonts",
"options": {"maxAge": "7d"}
},
"/sdkjs": {
"path": "/var/www/onlyoffice/documentserver/sdkjs",
"options": {"maxAge": "7d"}
},
"/web-apps": {
"path": "/var/www/onlyoffice/documentserver/web-apps",
"options": {"maxAge": "7d"}
},
"/welcome": {
"path": "/var/www/onlyoffice/documentserver/server/welcome",
"options": {"maxAge": "7d"}
},
"/info": {
"path": "/var/www/onlyoffice/documentserver/server/info",
"options": {"maxAge": "7d"}
},
"/sdkjs-plugins": {
"path": "/var/www/onlyoffice/documentserver/sdkjs-plugins",
"options": {"maxAge": "7d"}
},
"/dictionaries": {
"path": "/var/www/onlyoffice/documentserver/dictionaries",
"options": {"maxAge": "7d"}
}
}
},
"utils": {
"utils_common_fontdir": "/usr/share/fonts"
},
"sockjs": {
"sockjs_url": "/web-apps/vendor/sockjs/sockjs.min.js"
}
}
},
"license": {
"license_file": "/var/www/onlyoffice/documentserver/../Data/license.lic",
"warning_limit_percents": 70,
"packageType": 0
},
"FileConverter": {
"converter": {
"fontDir": "/usr/share/fonts",
"presentationThemesDir": "/var/www/onlyoffice/documentserver/sdkjs/slide/themes",
"x2tPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t",
"docbuilderPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder"
}
},
"SpellChecker": {
"server": {
"dictDir": "/var/www/onlyoffice/documentserver/dictionaries"
}
}
}

View File

@ -0,0 +1,8 @@
[supervisord]
pidfile=/tmp/supervisord.pi
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
[include]
files = supervisord.d/*.ini

View File

@ -0,0 +1,13 @@
[program:converter]
command=/var/www/onlyoffice/documentserver/server/FileConverter/converter
directory=/var/www/onlyoffice/documentserver/server/FileConverter
;user=ds
environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/onlyoffice/documentserver,NODE_DISABLE_COLORS=1,APPLICATION_NAME=onlyoffice
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,13 @@
[program:docservice]
command=/var/www/onlyoffice/documentserver/server/DocService/docservice
directory=/var/www/onlyoffice/documentserver/server/DocService
;user=ds
environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/onlyoffice/documentserver,NODE_DISABLE_COLORS=1
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,13 @@
[program:metrics]
command=/var/www/onlyoffice/documentserver/server/Metrics/metrics ./config/config.js
directory=/var/www/onlyoffice/documentserver/server/Metrics
;user=ds
environment=NODE_DISABLE_COLORS=1
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,13 @@
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
directory=/
;user=nginx
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,2 @@
[group:ds]
programs=docservice,converter,metrics,nginx

View File

@ -0,0 +1,5 @@
[onlyoffice]
baseurl = http://download.onlyoffice.com/repo/centos/main/noarch/
gpgcheck = 1
gpgkey = https://raw.githubusercontent.com/ONLYOFFICE/repo/master/centos/RPM-GPG-KEY-ONLYOFFICE
name = Only Office Repository

View File

@ -0,0 +1,8 @@
#!/bin/sh
set -euo pipefail
[[- template "common/vault.mkpgrole.sh.tpl"
dict "ctx" .
"config" (dict "role" .oo.instance "database" "postgres")
]]

View File

@ -0,0 +1,193 @@
[[ $c := merge .oo.ds . -]]
job [[ .oo.instance | toJSON ]] {
[[ template "common/job_start.tpl" $c ]]
group "onlyoffice" {
network {
mode = "bridge"
# This can be used to ensure rabbitmq has a stable hostname
# Even if for now, we do not persist rabbitmq data
hostname = "[[ .oo.instance ]][[ $c.consul.suffix ]]"
}
volume "ds" {
type = [[ .oo.volumes.ds.type | toJSON ]]
source = [[ .oo.volumes.ds.source | toJSON ]]
access_mode = "single-node-writer"
attachment_mode = "file-system"
}
service {
name = "[[ .oo.instance ]][[ $c.consul.suffix ]]"
port = 8084
[[ template "common/connect.tpl" $c ]]
check {
name = "health"
type = "http"
path = "/healthcheck"
expose = true
interval = "10s"
timeout = "5s"
check_restart {
limit = 90
grace = "2m"
}
}
tags = [
[[- if $c.traefik.enabled ]]
"[[ $c.traefik.instance ]].enable=true",
"[[ $c.traefik.instance ]].http.routers.[[ .oo.instance ]][[ $c.consul.suffix ]].rule=Host(`[[ (urlParse .oo.ds.public_url).Hostname ]]`)
[[- if not (regexp.Match "^/?$" (urlParse .oo.ds.public_url).Path) ]] && PathPrefix(`[[ (urlParse .oo.ds.public_url).Path ]]`)[[ end ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .oo.instance ]][[ $c.consul.suffix ]].entrypoints=[[ join $c.traefik.entrypoints "," ]]",
[[- if not (regexp.Match "^/?$" (urlParse .oo.ds.public_url).Path) ]]
"[[ $c.traefik.instance ]].http.middlewares.[[ .oo.instance ]][[ $c.consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .oo.ds.public_url).Path ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .oo.instance ]][[ $c.consul.suffix ]].middlewares=[[ .oo.instance ]][[ $c.consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- else ]]
"[[ $c.traefik.instance ]].http.routers.[[ .oo.instance ]][[ $c.consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- end ]]
[[- end ]]
]
}
task "documentserver" {
driver = [[ $c.nomad.driver | toJSON ]]
leader = true
config {
image = [[ .oo.ds.image | toJSON ]]
pids_limit = 100
readonly_rootfs = true
volumes = [
"secrets/production-linux.json:/etc/onlyoffice/documentserver/production-linux.json:ro",
"local/metrics.js:/var/www/onlyoffice/documentserver/server/Metrics/config/config.js:ro",
"secrets/nginx.conf:/etc/nginx/nginx.conf:ro"
]
[[ template "common/tmpfs.tpl" "/tmp" ]]
}
vault {
policies = ["[[ .oo.instance ]][[ $c.consul.suffix ]]"]
disable_file = true
env = false
}
env {
[[ template "common/proxy_env.tpl" $c ]]
}
[[ template "common/file_env.tpl" $c.env ]]
template {
data =<<_EOT
[[ template "onlyoffice-documentserver/production-linux.json.tpl" . ]]
_EOT
destination = "secrets/production-linux.json"
uid = 100000
gid = 100990
perms = 640
}
template {
data =<<_EOT
[[ template "onlyoffice-documentserver/metrics.js.tpl" . ]]
_EOT
destination = "local/metrics.js"
}
template {
data =<<_EOT
[[ template "onlyoffice-documentserver/nginx.conf.tpl" . ]]
_EOT
destination = "secrets/nginx.conf"
uid = 100000
gid = 100990
perms = 640
}
volume_mount {
volume = "ds"
destination = "/data"
}
[[ template "common/resources.tpl" $c.resources ]]
}
[[ template "common/task.wait_for.tpl" $c ]]
task "redis" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 2967
lifecycle {
hook = "prestart"
sidecar = true
}
config {
image = "redis:alpine"
pids_limit = 20
readonly_rootfs = true
args = ["/local/redis.conf"]
}
template {
data =<<_EOT
bind 127.0.0.1
maxmemory {{ env "NOMAD_MEMORY_LIMIT" | parseInt | subtract 5 }}mb
databases 1
save ""
appendonly no
_EOT
destination = "local/redis.conf"
}
resources {
cpu = 10
memory = 20
}
}
[[ $c := merge .oo.rabbitmq . ]]
task "rabbitmq" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 100
lifecycle {
hook = "prestart"
sidecar = true
}
config {
image = [[ $c.image | toJSON ]]
pids_limit = 100
readonly_rootfs = true
volumes = [
"local/rabbitmq.conf:/etc/rabbitmq/conf.d/30-oods.conf"
]
[[ template "common/tmpfs.tpl" dict "target" "/var/lib/rabbitmq" "size" 20000000]]
}
template {
data = <<_EOT
listeners.tcp.1 = 127.0.0.1:5672
# Set watermark to 70% of the mem allocated to the container
vm_memory_high_watermark.absolute = [[ mul .oo.rabbitmq.resources.memory 734003 ]]
_EOT
destination = "local/rabbitmq.conf"
}
[[ template "common/file_env.tpl" $c.env ]]
[[ template "common/resources.tpl" $c.resources ]]
}
}
}

1
prep.d/10-mv_conf.sh Executable file
View File

@ -0,0 +1 @@
[[ template "common/mv_conf.sh.tpl" dict "ctx" . "services" (dict "onlyoffice-docserver" .oo.instance) ]]

18
prep.d/20-rand-keys.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
set -euo pipefail
# Initialize random passwords if needed
if ! vault kv list [[ .vault.prefix ]]kv/service 2>/dev/null | grep -q -E '^[[ .oo.instance ]]$'; then
vault kv put [[ .vault.prefix ]]kv/service/[[ .oo.instance ]] \
secret_key=$(pwgen -s -n 50 1) \
link_secret=$(pwgen -s -n 50 1)
fi
for PWD in secret_key link_secret; do
if ! vault kv get -field ${PWD} [[ .vault.prefix ]]kv/service/[[ .oo.instance ]] >/dev/null 2>&1; then
vault kv patch [[ .vault.prefix ]]kv/service/[[ .oo.instance ]] \
${PWD}=$(pwgen -s -n 50 1)
fi
done

8
templates/metrics.js.tpl Normal file
View File

@ -0,0 +1,8 @@
{
port: 8125,
address: "127.0.0.1",
mgmt_address: "127.0.0.1",
mgmt_port: 8126,
flushInterval: 600000,
backends: [ "./backends/console" ]
}

72
templates/nginx.conf.tpl Normal file
View File

@ -0,0 +1,72 @@
worker_processes auto;
error_log /dev/stderr info;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_temp_path /tmp/proxy_temp;
client_body_temp_path /tmp/client_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
keepalive_timeout 65;
upstream docservice {
server unix:/tmp/oods.sock;
}
map $http_host $this_host {
"" $host;
default $http_host;
}
map $http_x_forwarded_proto $the_scheme {
default $http_x_forwarded_proto;
"" $scheme;
}
map $http_x_forwarded_host $the_host {
default $http_x_forwarded_host;
"" $this_host;
}
map $http_upgrade $proxy_connection {
default upgrade;
"" close;
}
map $http_x_forwarded_prefix $the_prefix {
default $http_x_forwarded_prefix;
}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_socket_keepalive on;
client_max_body_size 100m;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
server {
listen 127.0.0.1:8084 default_server;
server_tokens off;
set $secure_link_secret [[ .oo.ds.link_secret ]];
include /etc/nginx/includes/ds-docservice.conf;
include /etc/nginx/includes/ds-mime.types.conf;
}
}

View File

@ -0,0 +1,107 @@
{
"log": {
"filePath": "/etc/onlyoffice/documentserver/log4js/development.json"
},
"storage": {
"fs": {
"folderPath": "/data/files",
"secretString": "[[ .oo.ds.link_secret ]]"
}
},
"wopi": {
"htmlTemplate" : "/data/wopi"
},
"services": {
"CoAuthoring": {
[[- if and (has .oo.ds "secret_key") (ne .oo.ds.secret_key "") ]]
"secret": {
"inbox": {
"string": "[[ .oo.ds.secret_key ]]"
},
"outbox": {
"string": "[[ .oo.ds.secret_key ]]"
}
},
"token": {
"enable": {
"browser": true,
"request": {
"inbox": true,
"outbox": true
}
}
},
[[- end ]]
"sql": {
"type": "[[ .oo.ds.database.type ]]",
"dbHost": "[[ .oo.ds.database.host ]]",
"dbPort": [[ .oo.ds.database.port ]],
"dbName": "[[ .oo.ds.database.name ]]",
"dbUser": "[[ .oo.ds.database.user ]]",
"dbPass": "[[ .oo.ds.database.password ]]"
},
"autoAssembly": {
"enable": true,
"interval": "5m"
},
"server": {
"port": "/tmp/oods.sock",
"newFileTemplate" : "/data/templates",
"static_content": {
"/fonts": {
"path": "/var/www/onlyoffice/documentserver/fonts",
"options": {"maxAge": "7d"}
},
"/sdkjs": {
"path": "/var/www/onlyoffice/documentserver/sdkjs",
"options": {"maxAge": "7d"}
},
"/web-apps": {
"path": "/var/www/onlyoffice/documentserver/web-apps",
"options": {"maxAge": "7d"}
},
"/welcome": {
"path": "/var/www/onlyoffice/documentserver/server/welcome",
"options": {"maxAge": "7d"}
},
"/info": {
"path": "/var/www/onlyoffice/documentserver/server/info",
"options": {"maxAge": "7d"}
},
"/sdkjs-plugins": {
"path": "/var/www/onlyoffice/documentserver/sdkjs-plugins",
"options": {"maxAge": "7d"}
},
"/dictionaries": {
"path": "/var/www/onlyoffice/documentserver/dictionaries",
"options": {"maxAge": "7d"}
}
}
},
"utils": {
"utils_common_fontdir": "/usr/share/fonts"
},
"sockjs": {
"sockjs_url": "/web-apps/vendor/sockjs/sockjs.min.js"
}
}
},
"license": {
"license_file": "/var/www/onlyoffice/documentserver/../Data/license.lic",
"warning_limit_percents": 70,
"packageType": 0
},
"FileConverter": {
"converter": {
"fontDir": "/usr/share/fonts",
"presentationThemesDir": "/var/www/onlyoffice/documentserver/sdkjs/slide/themes",
"x2tPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/x2t",
"docbuilderPath": "/var/www/onlyoffice/documentserver/server/FileConverter/bin/docbuilder"
}
},
"SpellChecker": {
"server": {
"dictDir": "/var/www/onlyoffice/documentserver/dictionaries"
}
}
}

49
variables.yml Normal file
View File

@ -0,0 +1,49 @@
---
oo:
instance: onlyoffice-docserver
ds:
image: danielberteaud/onlyoffice-docserver:latest
env: {}
resources:
cpu: 200
memory: 512
public_url: https://oods.example.org
secret_key: '{{ with secret "[[ .vault.prefix ]]kv/service/[[ .oo.instance ]]" }}{{ .Data.data.secret_key }}{{ end }}'
link_secret: '{{ with secret "[[ .vault.prefix ]]kv/service/[[ .oo.instance ]]" }}{{ .Data.data.link_secret }}{{ end }}'
database:
type: postgres
host: localhost
port: 5432
name: '[[ .oo.instance ]]'
user: '{{- with secret "[[ .vault.prefix ]]database/creds/[[ .oo.instance ]]" }}{{ .Data.username }}{{ end }}'
password: '{{- with secret "[[ .vault.prefix ]]database/creds/[[ .oo.instance ]]" }}{{ .Data.password }}{{ end }}'
traefik:
enabled: true
base_middlewares: []
#- rate-limit-std@file
#- inflight-std@file
#- security-headers@file
#- forward-headers@file
#- hsts@file
#- compression@file
wait_for:
- service: 'master.postgres[[ .consul.suffix ]]'
consul:
connect:
upstreams:
- destination_name: 'postgres[[ .consul.suffix ]]'
local_bind_port: 5432
rabbitmq:
image: rabbitmq:alpine
env: {}
resources:
cpu: 80
memory: 128
volumes:
ds:
type: csi
source: '[[ .oo.instance ]]-data'

View File

@ -0,0 +1,8 @@
path "[[ .vault.prefix ]]kv/data/service/[[ .oo.instance ]]" {
capabilities = ["read"]
}
path "[[ .vault.prefix ]]database/creds/[[ .oo.instance ]]" {
capabilities = ["read"]
}