Validated Patterns chart for non-production S4 object storage (dev/demo) with External Secrets and bucket provisioning. For production S3 on OpenShift, use openshift-data-foundations (ODF).
Wraps the S4 Helm chart with External Secrets for deployment credentials and imperative Jobs to provision S3 buckets.
Not for production. This chart deploys S4 for development, test, and demonstration. It is not intended for production S3 object storage. For production S3 workloads on OpenShift, use the Validated Patterns openshift-data-foundations chart (ODF on the VP catalog:
chart: openshift-data-foundationsfrom charts.validatedpatterns.io).
Defaults assume an OpenShift cluster: OpenShift Route for the Web UI (Ingress disabled), cluster-default StorageClass for PVCs, pinned S4 image tag, and restricted pod security contexts compatible with the restricted-v2 SCC.
Bucket create/destroy logic is shipped as an Ansible playbook in a ConfigMap (playbooks/s4-buckets.yml), mounted into the utility-container (ansible + amazon.aws). Default variables live in vars/defaults.yml on the mount (ConfigMap key vars.defaults.yml); the Job and CronJob pass s4Role.buckets, s4-credentials, and optional s4Role.destroy at runtime.
The playbook and variable model were adapted from eduffy-redhat/s4-role. Credit to Evan Duffy (Red Hat) for the original Ansible role and approach to managing buckets on an S4 endpoint.
S4 has two access paths, stored in two Vault secrets and merged into one Kubernetes Secret for the upstream s4 subchart (no subchart changes):
| Access path | Keys | Vault secret (example) |
|---|---|---|
| Web UI | UI_USERNAME, UI_PASSWORD [, JWT_SECRET] |
s4-ui-credentials |
| S3 API | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
s4-api-credentials |
External Secrets Operator uses one ExternalSecret (s4Credentials.secretName) with two dataFrom.extract entries (UI and API Vault paths). That avoids creationPolicy: Merge, which fails if the target Secret does not exist yet when both resources reconcile in parallel. The resulting secret is passed to the unmodified s4 subchart via s4.s3.existingSecret and is used by bucket Jobs. The API Vault path is the RGW identity for the endpoint, provisioning, and application consumers.
Default examples use s4admin for the UI user and S3 access key id; the UI password and API secret key are generated (advancedPolicy).
Copy examples/secrets/values-secret.v2.yaml into your pattern common/examples/secrets/ and run your pattern secrets tooling (e.g. ./scripts/make-secrets.sh). Then point the chart at the Vault paths with a values overlay like examples/chart-secret-values.yaml.
# NEVER COMMIT REAL SECRETS TO GIT
#
# Validated Patterns secrets example for vp-s4-storage.
# Use from your pattern repo with the secrets tooling, e.g.:
# ./scripts/make-secrets.sh -f common/examples/secrets/values-secret.v2.yaml
#
# Two Vault secrets (Web UI vs S3 API) merge into Kubernetes Secret s4-credentials.
# Wire Vault paths into the chart via examples/chart-secret-values.yaml.
version: "2.0"
backingStore: vault
vaultPolicies:
basicPolicy: |
length=10
rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 }
rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 }
rule "charset" { charset = "0123456789" min-chars = 1 }
advancedPolicy: |
length=20
rule "charset" { charset = "abcdefghijklmnopqrstuvwxyz" min-chars = 1 }
rule "charset" { charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" min-chars = 1 }
rule "charset" { charset = "0123456789" min-chars = 1 }
rule "charset" { charset = "!@#$%^&*" min-chars = 1 }
secrets:
- name: s4-ui-credentials
vaultPrefixes:
- global
fields:
- name: UI_USERNAME
value: s4admin
onMissingValue: error
- name: UI_PASSWORD
onMissingValue: generate
override: true
vaultPolicy: advancedPolicy
- name: s4-api-credentials
vaultPrefixes:
- global
fields:
- name: AWS_ACCESS_KEY_ID
value: s4admin
onMissingValue: error
- name: AWS_SECRET_ACCESS_KEY
onMissingValue: generate
override: true
vaultPolicy: advancedPolicy
# Overlay for vp-s4-storage after running pattern secrets tooling.
# Adjust vaultKey paths to match your hub/site prefix (global, cluster, etc.).
#
# secret/data/global/s4-ui-credentials
# secret/data/global/s4-api-credentials
s4UICredentials:
vaultKey: secret/data/global/s4-ui-credentials
s4APICredentials:
vaultKey: secret/data/global/s4-api-credentials
When this chart is published to charts.validatedpatterns.io, add a namespace and Argo CD application under clusterGroup in your hub or site values (same style as other vp-* catalog charts):
# Example clusterGroup fragment for a Validated Patterns hub (or site) values file.
# Assumes vp-s4-storage is published on https://charts.validatedpatterns.io
# (same catalog as vp-rbac, aap-config, vp-stakater-reloader, etc.).
#
# Merge into values-global.yaml / values-hub.yaml under clusterGroup:.
# Run secrets tooling first (see examples/secrets/values-secret.v2.yaml).
clusterGroup:
namespaces:
- s4-storage
# argoProject must already be listed in clusterGroup.argoProjects
argoProjects:
- hub
applications:
vp-s4-storage:
name: vp-s4-storage
namespace: vp-s4-storage
argoProject: hub
chart: vp-s4-storage
chartVersion: 0.2.*
overrides:
# Vault paths from examples/secrets/values-secret.v2.yaml (adjust prefix as needed)
- name: s4UICredentials.vaultKey
value: secret/data/global/s4-ui-credentials
- name: s4APICredentials.vaultKey
value: secret/data/global/s4-api-credentials
# Bucket names for the imperative Job/CronJob playbook
- name: s4Role.buckets[0]
value: my-app-data
- name: s4Role.buckets[1]
value: my-app-logs
# vp-rbac Role/RoleBinding namespace must match the Argo CD app namespace
- name: vp-rbac.serviceAccounts.vp-s4-storage-sa.namespace
value: vp-s4-storage
- name: vp-rbac.roles.external-secrets-validator.namespace
value: vp-s4-storage
# ConsoleLink is enabled by default; href uses global.localClusterDomain from values-global.yaml
# from values-global.yaml (same as clustergroup Argo CD ConsoleLinks). Optional overrides:
# - name: consoleLink.href
# value: https://s4.apps.mycluster.example.com
# - name: s4.route.host
# value: s4.apps.mycluster.example.com
# Optional Route hostnames (both Routes enabled by default; see examples/clustergroup-route-overrides.yaml)
# - name: s4.route.host
# value: s4.apps.mycluster.example.com
# - name: s4.route.s3Api.host
# value: s3-s4.apps.mycluster.example.com
Argo CD pulls chart: vp-s4-storage and chartVersion: 0.1.* from the Validated Patterns Helm repo (no repoURL required unless you override the default catalog URL). Use overrides for Vault keys, bucket lists, and optional Route hostnames. Ensure the argoProject you reference is listed in clusterGroup.argoProjects.
Routes are rendered by the upstream s4 subchart (charts/s4/templates/route.yaml and route-s3.yaml). This wrapper passes settings under s4.route.* in Helm values or Argo CD overrides.
Full examples (uncomment the scenario you need):
# Helm values examples for OpenShift Routes (s4 subchart keys under s4.route.*)
# Use with: helm install ... -f examples/route-values.yaml
# Or merge selected keys into a pattern values overlay / clusterGroup overrides.
# -----------------------------------------------------------------------------
# 1) Default — Web UI + S3 API Routes, cluster-assigned FQDNs (chart defaults)
# -----------------------------------------------------------------------------
# s4:
# route:
# enabled: true
# host: ""
# annotations:
# haproxy.router.openshift.io/timeout: 600s
# s3Api:
# enabled: true
# host: ""
# annotations:
# haproxy.router.openshift.io/timeout: 600s
# tls:
# termination: edge
# insecureEdgeTerminationPolicy: Allow # HTTP :80 and HTTPS :443
# -----------------------------------------------------------------------------
# 2) Web UI — custom FQDN (DNS must point at the cluster ingress/router)
# -----------------------------------------------------------------------------
# s4:
# route:
# enabled: true
# host: s4.apps.mycluster.example.com
# tls:
# termination: edge
# insecureEdgeTerminationPolicy: Redirect
# -----------------------------------------------------------------------------
# 3) Web UI + S3 API — custom FQDNs on both Routes
# -----------------------------------------------------------------------------
# s4:
# route:
# enabled: true
# host: s4.apps.mycluster.example.com
# s3Api:
# enabled: true
# host: s3-s4.apps.mycluster.example.com
# annotations:
# haproxy.router.openshift.io/timeout: 600s
# tls:
# termination: edge
# insecureEdgeTerminationPolicy: Allow # or Redirect for HTTPS-only
# -----------------------------------------------------------------------------
# 4) S3 API internal only (Web UI Route still public)
# -----------------------------------------------------------------------------
# s4:
# route:
# enabled: true
# s3Api:
# enabled: false
# -----------------------------------------------------------------------------
# 5) Disable Routes (Ingress or in-cluster / port-forward only)
# -----------------------------------------------------------------------------
# s4:
# route:
# enabled: false
# ingress:
# enabled: true
# className: openshift-default
# hosts:
# - host: s4.apps.mycluster.example.com
# paths:
# - path: /
# pathType: Prefix
Copy override blocks into clusterGroup.applications.vp-s4-storage.overrides:
# clusterGroup overrides only — merge into clusterGroup.applications.vp-s4-storage.overrides
# Chart defaults enable both Web UI and S3 API Routes (cluster-assigned FQDNs).
#
# Argo CD override names use dotted Helm value paths. Escape dots in annotation keys
# with a backslash in the override name.
# --- Defaults: no overrides (both Routes enabled, auto hostname) ---
# --- Web UI: pinned FQDN ---
overrides_web_ui_host:
- name: s4.route.host
value: s4.apps.mycluster.example.com
# --- S3 API: pinned FQDN ---
overrides_s3_api_host:
- name: s4.route.s3Api.host
value: s3-s4.apps.mycluster.example.com
# --- Web UI + S3 API: pinned FQDNs on both ---
overrides_web_ui_and_s3_api_hosts:
- name: s4.route.host
value: s4.apps.mycluster.example.com
- name: s4.route.s3Api.host
value: s3-s4.apps.mycluster.example.com
# --- Web UI: upload timeout annotation ---
overrides_web_ui_annotation:
- name: s4.route.annotations.haproxy\.router\.openshift\.io/timeout
value: 600s
# --- S3 API Route off (in-cluster Service only; Web UI Route unchanged) ---
overrides_s3_api_internal_only:
- name: s4.route.s3Api.enabled
value: "false"
# --- All OpenShift Routes off ---
overrides_disable_routes:
- name: s4.route.enabled
value: "false"
Typical override names:
| Goal | Override |
|---|---|
| Custom Web UI FQDN | s4.route.host |
| Router annotation | s4.route.annotations.haproxy\.router\.openshift\.io/timeout |
| Custom S3 API FQDN | s4.route.s3Api.host |
| Disable S3 API Route | s4.route.s3Api.enabled: "false" |
| No Routes | s4.route.enabled: "false" |
Rendered resources use the release namespace (e.g. s4-storage). Two Routes are created by default: Web UI (web-ui, port 5000) and S3 API (s3-api, port 7480, object name suffix -api).
OpenShift defaults expose both the Web UI and S3 API on separate edge-terminated Routes (s4.route.enabled and s4.route.s3Api.enabled, both true). Each gets its own FQDN unless you set s4.route.host or s4.route.s3Api.host.
| Setting | Behavior |
|---|---|
s4.route.host empty |
OpenShift assigns a predictable hostname (see below). |
s4.route.host set |
Route uses that FQDN; DNS must resolve to the cluster ingress/router. |
| TLS | Edge termination (HTTPS at the router; HTTP to the pod). |
Yes. If you do not set s4.route.host or s4.route.s3Api.host, OpenShift fills spec.host on each Route using the cluster ingress domain. The pattern is:
<route.metadata.name>-<namespace>.<ingress-domain>
Step 1 — Route object names (from Helm, before apply)
The s4 subchart names routes from s4.fullname:
| Route | metadata.name |
|---|---|
| Web UI | {s4.fullname} |
| S3 API | {s4.fullname}-api |
{s4.fullname} is computed as:
s4.fullnameOverrideif set, elseRelease.Nameif it contains the substrings4, else{Release.Name}-s4
For a typical Validated Patterns app (name: vp-s4-storage, Argo CD release vp-s4-storage), Release.Name contains s4, so:
| Resource | Name |
|---|---|
| Web UI Route | vp-s4-storage |
| S3 API Route | vp-s4-storage-api |
| Service (in-cluster S3) | vp-s4-storage |
If your release name does not contain s4 (e.g. release storage), use storage-s4 and storage-s4-api instead.
Step 2 — Ingress domain (cluster constant)
oc get ingresses.config cluster -o jsonpath='{.status.domain}{"\n"}'Example output: apps.ocp4.example.com (varies per cluster; set by install or Ingress.config.openshift.io).
Step 3 — Assemble the FQDN
With namespace s4-storage and ingress domain apps.ocp4.example.com:
Web UI: vp-s4-storage-s4-storage.apps.ocp4.example.com
S3 API: vp-s4-storage-api-s4-storage.apps.ocp4.example.com
Template:
https://{route-name}-{namespace}.{ingress-domain} # Web UI or S3 Route (edge TLS)
http://{s4.fullname}.{namespace}.svc:7480 # in-cluster S3 only
After deploy, confirm:
INGRESS_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.status.domain}')
NS=s4-storage
echo "Web UI: vp-s4-storage-${NS}.${INGRESS_DOMAIN}"
echo "S3 API: vp-s4-storage-api-${NS}.${INGRESS_DOMAIN}"
oc get route -n "${NS}" -l app.kubernetes.io/name=s4 -o custom-columns=NAME:.metadata.name,HOST:.spec.hostLog in to the Web UI with s4-credentials (UI_USERNAME / UI_PASSWORD). Do not use the Web UI Route URL as an S3 endpoint.
Use the S3 keys in s4-credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), region us-east-1. The same secret backs the RGW process, bucket Jobs (s4Role.buckets), and application clients.
In-cluster (typical for pods on the same cluster):
http://vp-s4-storage.s4-storage.svc:7480
export AWS_ACCESS_KEY_ID=$(oc get secret s4-credentials -n s4-storage -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d)
export AWS_SECRET_ACCESS_KEY=$(oc get secret s4-credentials -n s4-storage -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d)
aws --endpoint-url http://vp-s4-storage.s4-storage.svc:7480 s3 lsOutside the cluster (default chart also creates an S3 API Route):
The S3 API Route defaults to s4.route.s3Api.tls.insecureEdgeTerminationPolicy: Allow, so clients can use HTTP on port 80 or HTTPS on port 443 (edge termination). The Web UI Route still redirects HTTP to HTTPS (Redirect).
S3_HOST=$(oc get route vp-s4-storage-api -n s4-storage -o jsonpath='{.spec.host}')
aws --endpoint-url "http://${S3_HOST}" s3 ls
# or: aws --endpoint-url "https://${S3_HOST}" s3 lsOr set s4.route.s3Api.host to a stable name (e.g. s3-s4.apps.mycluster.example.com) in values or clusterGroup overrides. Set s4.route.s3Api.tls.insecureEdgeTerminationPolicy: Redirect to force HTTPS only. Restrict access with network policy; the S3 Route exposes the API outside the cluster.
Default s4.commonAnnotations sets argocd.argoproj.io/sync-wave: "2" on S4 subchart resources (Deployment, Service, PVC, and related objects). Umbrella-chart resources keep their existing waves: ExternalSecrets and the validation Job at 1, bucket ConfigMap at 2, bootstrap Job at 3, CronJob at 5. That ensures credentials exist before the S4 Deployment starts and the bootstrap Job still runs after the workload is up.
Default s4.route.s3Api.tls.insecureEdgeTerminationPolicy is Allow so the S3 API Route serves HTTP on port 80 as well as HTTPS on port 443. The Web UI Route remains Redirect (HTTP to HTTPS).
When the Web UI Route is enabled (consoleLink.enabled defaults to true), the chart creates a cluster ConsoleLink in the console Application menu (consoleLink.section: Storage), matching the Validated Patterns clustergroup style used for Argo CD (common/clustergroup/templates/plumbing/argocd.yaml) and Vault ConsoleLinks. The spec.href URL is s4.route.host when set, else https://{route}-{namespace}.{ingress-domain} with ingress-domain from coalesce(consoleLink.ingressDomain, global.localClusterDomain) — global.localClusterDomain is set by the pattern framework in values-global.yaml. Override with consoleLink.href if needed. Set consoleLink.enabled: false to disable. The icon is the 64×64 PNG from rh-aiservices-bu/s4 (assets/s4-icon-64x64.png), bundled in the chart as a data URI unless consoleLink.imageURL is set.
Homepage: https://github.com/rh-aiservices-bu/s4
| Repository | Name | Version |
|---|---|---|
| file://charts/s4 | s4 | 0.1.0 |
| https://charts.validatedpatterns.io | vp-rbac | 0.1.* |
| Key | Type | Default | Description |
|---|---|---|---|
| configJob.activeDeadlineSeconds | int | 3600 |
|
| configJob.configTimeout | int | 1800 |
|
| configJob.disabled | bool | false |
|
| configJob.image | string | "quay.io/validatedpatterns/utility-container:latest" |
|
| configJob.imagePullPolicy | string | "IfNotPresent" |
|
| configJob.s4ReadyTimeoutSeconds | int | 600 |
|
| configJob.schedule | string | "10 */2 * * *" |
|
| consoleLink.enabled | bool | true |
|
| consoleLink.href | string | "" |
|
| consoleLink.imageURL | string | "" |
|
| consoleLink.ingressDomain | string | "" |
|
| consoleLink.section | string | "Storage" |
|
| consoleLink.text | string | "S4 Web UI" |
|
| s4.auth.cookieRequireHttps | bool | true |
|
| s4.auth.enabled | bool | true |
|
| s4.commonAnnotations."argocd.argoproj.io/sync-wave" | string | "2" |
|
| s4.image.pullPolicy | string | "IfNotPresent" |
|
| s4.image.repository | string | "quay.io/rh-aiservices-bu/s4" |
|
| s4.image.tag | string | "0.3.2" |
|
| s4.ingress.enabled | bool | false |
|
| s4.ingress.s3Api.enabled | bool | false |
|
| s4.podSecurityContext.runAsNonRoot | bool | true |
|
| s4.route.annotations."haproxy.router.openshift.io/timeout" | string | "600s" |
|
| s4.route.enabled | bool | true |
|
| s4.route.s3Api.annotations."haproxy.router.openshift.io/timeout" | string | "600s" |
|
| s4.route.s3Api.enabled | bool | true |
|
| s4.route.s3Api.tls.insecureEdgeTerminationPolicy | string | "Allow" |
|
| s4.route.s3Api.tls.termination | string | "edge" |
|
| s4.route.tls.insecureEdgeTerminationPolicy | string | "Redirect" |
|
| s4.route.tls.termination | string | "edge" |
|
| s4.s3.existingSecret | string | "s4-credentials" |
|
| s4.securityContext.allowPrivilegeEscalation | bool | false |
|
| s4.securityContext.capabilities.drop[0] | string | "ALL" |
|
| s4.securityContext.runAsNonRoot | bool | true |
|
| s4.securityContext.seccompProfile.type | string | "RuntimeDefault" |
|
| s4.service.nodePort.enabled | bool | false |
|
| s4.service.type | string | "ClusterIP" |
|
| s4.serviceAccount.create | bool | true |
|
| s4.storage.data.accessMode | string | "ReadWriteOnce" |
|
| s4.storage.data.size | string | "10Gi" |
|
| s4.storage.data.storageClass | string | "" |
|
| s4.storage.localStorage.enabled | bool | false |
|
| s4APICredentials.vaultKey | string | "secret/data/global/s4-api-credentials" |
|
| s4Credentials.secretName | string | "s4-credentials" |
|
| s4Role.buckets | list | [] |
|
| s4Role.destroy | bool | false |
|
| s4Role.endpoint.address | string | "" |
|
| s4Role.endpoint.port | int | 7480 |
|
| s4Role.endpoint.protocol | string | "http" |
|
| s4UICredentials.vaultKey | string | "secret/data/global/s4-ui-credentials" |
|
| secretStore.kind | string | "ClusterSecretStore" |
|
| secretStore.name | string | "vault-backend" |
|
| serviceAccountName | string | "vp-s4-storage-sa" |
|
| serviceAccountNamespace | string | "" |
|
| validationJob.activeDeadlineSeconds | int | 3600 |
|
| validationJob.disabled | bool | false |
|
| vp-rbac.roles.external-secrets-validator.namespace | string | "" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].apiGroups[0] | string | "external-secrets.io" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].apiGroups[1] | string | "" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].resources[0] | string | "externalsecrets" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].resources[1] | string | "secrets" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].verbs[0] | string | "get" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].verbs[1] | string | "list" |
|
| vp-rbac.roles.external-secrets-validator.rules[0].verbs[2] | string | "watch" |
|
| vp-rbac.serviceAccounts.vp-s4-storage-sa.namespace | string | "" |
|
| vp-rbac.serviceAccounts.vp-s4-storage-sa.roleBindings.clusterRoles | list | [] |
|
| vp-rbac.serviceAccounts.vp-s4-storage-sa.roleBindings.roles[0] | string | "external-secrets-validator" |
Autogenerated from chart metadata using helm-docs v1.14.2