Helm¶
SolAr Helm Chart¶
Helm chart for Solution Arsenal (SolAr) - An application catalog based on Open Component Model packages (ocm.software) and fleet rollout management for these solutions onto Kubernetes Clusters.
Homepage: https://solar.opendefense.cloud
Prerequisites¶
- Kubernetes 1.33+ (enforced by the chart's
kubeVersion) - A Helm release whose supported version skew covers your cluster. Each Helm minor supports the Kubernetes minor it was built against plus the three before it, so Helm can be too new as well as too old: Kubernetes 1.33 needs Helm 3.18.x-3.21.x or 4.0.x-4.2.x
helm templateand--dry-run=clientadditionally need Helm 4.1+ or an explicit--kube-version, because every Helm 3 release (including 3.22) and Helm 4.0 assume Kubernetes v1.20 when they cannot reach a cluster- cert-manager (for TLS certificate management)
- Flux
source-controllerandhelm-controller(SolAr's renderer emitsOCIRepository/HelmReleaseresources that Flux reconciles to actually deploy releases) - trust-manager, only if your OCI registries use a private/self-signed CA (set
caBundle.enabled=true)
Install from OCI Registry¶
SolAr Helm charts are published to GitHub Container Registry as OCI artifacts.
# Pull and install the latest version
helm install solar oci://ghcr.io/opendefensecloud/charts/solar \
--namespace solar-system \
--create-namespace
# Install a specific version
helm install solar oci://ghcr.io/opendefensecloud/charts/solar --version 0.1.0 \
--namespace solar-system \
--create-namespace
# Install with custom values
helm install solar oci://ghcr.io/opendefensecloud/charts/solar \
--namespace solar-system \
--create-namespace \
-f custom-values.yaml
Install from Source¶
For development or testing, install directly from the source repository:
# Clone the repository
git clone https://github.com/opendefensecloud/solution-arsenal.git
cd solution-arsenal
# Install from local chart
helm install solar ./charts/solar --namespace solar-system --create-namespace
Install cert-manager (if not already installed)¶
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
Components¶
The chart deploys three main components:
- API Server - Extension API server providing custom resources
- Controller Manager - Reconciles the custom resources
- etcd - Dedicated storage backend for the API server
Examples¶
Basic Installation¶
helm install solar oci://ghcr.io/opendefensecloud/charts/solar \
--namespace solar-system \
--create-namespace
High Availability Setup¶
# ha-values.yaml
controller:
replicaCount: 3
args:
leaderElect: true
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/component: controller-manager
topologyKey: kubernetes.io/hostname
etcd:
replicaCount: 3
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/component: etcd
topologyKey: kubernetes.io/hostname
helm install solar oci://ghcr.io/opendefensecloud/charts/solar -f ha-values.yaml
Enable Metrics with Prometheus¶
# metrics-values.yaml
controller:
metrics:
enabled: true
serviceMonitor:
enabled: true
additionalLabels:
prometheus: kube-prometheus
helm install solar oci://ghcr.io/opendefensecloud/charts/solar -f metrics-values.yaml
Custom Storage Class¶
# storage-values.yaml
global:
storageClass: fast-ssd
etcd:
persistence:
size: 10Gi
helm install solar oci://ghcr.io/opendefensecloud/charts/solar -f storage-values.yaml
Using External etcd¶
# external-etcd-values.yaml
etcd:
enabled: false
apiserver:
args:
etcdServers: "http://external-etcd:2379"
helm install solar oci://ghcr.io/opendefensecloud/charts/solar -f external-etcd-values.yaml
Private Renderer Image¶
To pull the renderer image from a private registry, set
renderer.imagePullSecrets (and/or global.imagePullSecrets):
# private-renderer-values.yaml
renderer:
image:
repository: registry.example.com/solar-renderer
imagePullSecrets:
- name: solar-renderer-pull
helm install solar oci://ghcr.io/opendefensecloud/charts/solar -f private-renderer-values.yaml
The renderer Pod is spawned in the RenderTask's namespace (which equals
the owning Target's namespace), not in the controller-manager namespace.
The referenced Secret (type kubernetes.io/dockerconfigjson) must
therefore exist in every namespace where Targets are created.
Cross-namespace Secret references do not work.
Recommended distribution pattern — External Secrets Operator. A
ClusterSecretStore pointing at your existing secret backend plus a
ClusterExternalSecret with a namespaceSelector matching Target
namespaces will materialise the same dockerconfigjson Secret in every
selected namespace and keep it rotated centrally:
apiVersion: external-secrets.io/v1
kind: ClusterExternalSecret
metadata:
name: solar-renderer-pull
spec:
externalSecretName: solar-renderer-pull
namespaceSelector:
matchLabels:
solar.opendefense.cloud/tenant: "true"
externalSecretSpec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: my-secret-store
target:
name: solar-renderer-pull
template:
type: kubernetes.io/dockerconfigjson
data:
- secretKey: .dockerconfigjson
remoteRef:
key: registry/solar-renderer-pull
Alternatives that satisfy the same constraint: reflector (label-driven
Secret cloning), kyverno generate, Argo CD ApplicationSet, Flux
Kustomization, or — for single-namespace bring-up — a plain
kubectl create secret docker-registry.
Upgrading¶
# Upgrade to the latest version
helm upgrade solar oci://ghcr.io/opendefensecloud/charts/solar --namespace solar-system
# Upgrade to a specific version
helm upgrade solar oci://ghcr.io/opendefensecloud/charts/solar --version 0.2.0 --namespace solar-system
Uninstalling¶
helm uninstall solar --namespace solar-system
Configuration¶
Values¶
| Key | Type | Default | Description |
|---|---|---|---|
| apiserver.affinity | object | {} |
Affinity for pod assignment |
| apiserver.apiservice.groupPriorityMinimum | int | 2000 |
Group priority minimum |
| apiserver.apiservice.versionPriority | int | 100 |
Version priority |
| apiserver.args.auditLogMaxAge | int | 0 |
Audit log max age |
| apiserver.args.auditLogMaxBackup | int | 0 |
Audit log max backup |
| apiserver.args.auditLogPath | string | "-" |
Audit log path ("-" for stdout) |
| apiserver.args.disableAdmissionPlugins | string | "MutatingAdmissionPolicy" |
Comma-separated admission plugins to disable. MutatingAdmissionPolicy lists admissionregistration.k8s.io/v1, which host clusters below 1.36 do not serve, leaving the apiserver unready. Drop it once the minimum supported host cluster is 1.36. To run the plugin anyway, clear this value — do not enable it through extraArgs instead, as the apiserver rejects a plugin that appears in both the enable and disable lists. |
| apiserver.args.enablePriorityAndFairness | bool | false |
Enable priority and fairness |
| apiserver.args.etcdServers | string | "" |
etcd server URLs (auto-configured to internal etcd service if empty) |
| apiserver.args.securePort | int | 8443 |
Secure port for HTTPS |
| apiserver.command | list | ["/solar-apiserver"] |
Command to run in the container |
| apiserver.enabled | bool | true |
Enable API Server deployment |
| apiserver.extraArgs | object | {} |
Additional command-line arguments as key-value pairs |
| apiserver.extraEnv | list | [] |
Additional environment variables |
| apiserver.fullnameOverride | string | "" |
Override API Server full name |
| apiserver.image.pullPolicy | string | "IfNotPresent" |
Image pull policy |
| apiserver.image.repository | string | "ghcr.io/opendefensecloud/solar-apiserver" |
API Server image repository |
| apiserver.image.tag | string | "" |
API Server image tag (defaults to chart appVersion if not set) |
| apiserver.imagePullSecrets | list | [] |
Image pull secrets for API Server |
| apiserver.livenessProbe | object | {"httpGet":{"path":"/healthz","port":8443,"scheme":"HTTPS"},"initialDelaySeconds":20,"periodSeconds":20} |
Liveness probe configuration |
| apiserver.nameOverride | string | "" |
Override API Server name |
| apiserver.nodeSelector | object | {} |
Node selector for pod assignment |
| apiserver.podAnnotations | object | {} |
Pod annotations |
| apiserver.podLabels | object | {} |
Pod labels |
| apiserver.podSecurityContext | object | {"runAsNonRoot":true} |
Pod security context |
| apiserver.readinessProbe | object | {"httpGet":{"path":"/readyz","port":8443,"scheme":"HTTPS"},"initialDelaySeconds":5,"periodSeconds":10} |
Readiness probe configuration |
| apiserver.replicaCount | int | 1 |
Number of API Server replicas |
| apiserver.resources | object | {"limits":{"cpu":"500m","memory":"128Mi"},"requests":{"cpu":"10m","memory":"64Mi"}} |
Resource limits and requests |
| apiserver.securityContext | object | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} |
Container security context |
| apiserver.service.annotations | object | {} |
Service annotations |
| apiserver.service.port | int | 443 |
Service port |
| apiserver.service.targetPort | int | 8443 |
Service target port |
| apiserver.service.type | string | "ClusterIP" |
Service type |
| apiserver.serviceAccount.annotations | object | {} |
Service account annotations |
| apiserver.serviceAccount.create | bool | true |
Create service account |
| apiserver.serviceAccount.name | string | "" |
Service account name (auto-generated if not set) |
| apiserver.tolerations | list | [] |
Tolerations for pod assignment |
| caBundle | object | {"configMapName":"","enabled":false,"key":"trust-bundle.pem"} |
CA certificate bundle configuration. Mount a ConfigMap containing a CA bundle for TLS connections to registries. |
| caBundle.configMapName | string | "" |
Name of the ConfigMap containing the CA bundle |
| caBundle.enabled | bool | false |
Enable CA bundle mounting |
| caBundle.key | string | "trust-bundle.pem" |
Key in the ConfigMap (default: trust-bundle.pem) |
| certManager.certificate.duration | string | "2160h" |
Certificate duration |
| certManager.certificate.renewBefore | string | "720h" |
Renew before duration |
| certManager.enabled | bool | true |
Enable cert-manager integration (requires cert-manager to be installed) |
| certManager.issuer.acme.email | string | "" |
Email for ACME registration |
| certManager.issuer.acme.enabled | bool | false |
Enable ACME issuer |
| certManager.issuer.acme.privateKeySecretRef | string | "" |
Private key secret reference |
| certManager.issuer.acme.server | string | "" |
ACME server URL |
| certManager.issuer.ca.enabled | bool | false |
Enable CA issuer |
| certManager.issuer.ca.secretName | string | "" |
CA secret name |
| certManager.issuer.create | bool | true |
Create Issuer resource |
| certManager.issuer.kind | string | "Issuer" |
Issuer kind (Issuer or ClusterIssuer) |
| certManager.issuer.name | string | "" |
Issuer name (auto-generated if empty) |
| certManager.issuer.selfSigned | bool | true |
Use self-signed issuer |
| commonAnnotations | object | {} |
Common annotations applied to all resources |
| commonLabels | object | {} |
Common labels applied to all resources |
| controller.affinity | object | {} |
Affinity for pod assignment |
| controller.agent.apiserverCAFile | string | "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" |
PEM CA bundle for apiserverURL, rendered into each agent's kubeconfig. The default is the cluster CA, correct when target clusters reach the kube-apiserver without a TLS-terminating proxy. |
| controller.agent.apiserverURL | string | "" |
The SolAr cluster's kube-apiserver URL as target clusters reach it. Set it to give every Target an agent credential; empty disables the feature. |
| controller.agent.tokenTTL | string | "8760h" |
Lifetime requested for agent tokens. The cluster may cap it lower (kube-apiserver flag service-account-max-token-expiration); tokens are rotated before expiry. |
| controller.args.componentVersionAvailabilityGrace | string | "15m" |
How long a descriptor manifest must stay missing before its ComponentVersion turns Available=False. |
| controller.args.componentVersionAvailabilityInterval | string | "5m" |
How often each ComponentVersion's backing descriptor manifest is re-checked in its registry. |
| controller.args.enableHTTP2 | bool | false |
Enable HTTP/2 for metrics server |
| controller.args.healthProbeBindAddress | string | ":8081" |
Health probe bind address |
| controller.args.leaderElect | bool | false |
Enable leader election (set to true for HA) |
| controller.args.metricsBindAddress | string | "0" |
Metrics bind address (set to "0" to disable, ":8443" for HTTPS) |
| controller.args.metricsSecure | bool | true |
Serve metrics securely via HTTPS |
| controller.args.pprofBindAddress | string | "" |
Pprof bind address (empty to disable) |
| controller.args.registryBindingStrict | bool | false |
Enable strict registry binding mode. When true, rendering fails if a resource's registry host has no matching RegistryBinding. When false (default/relaxed), unmatched hosts use anonymous pull (no secretRef). |
| controller.command | list | ["/solar-controller-manager"] |
Command to run in the container |
| controller.enabled | bool | true |
Enable Controller Manager deployment |
| controller.extraArgs | object | {} |
Additional command-line arguments as key-value pairs |
| controller.extraEnv | list | [] |
Additional environment variables |
| controller.fullnameOverride | string | "" |
Override Controller Manager full name |
| controller.image.pullPolicy | string | "IfNotPresent" |
Image pull policy |
| controller.image.repository | string | "ghcr.io/opendefensecloud/solar-controller-manager" |
Controller Manager image repository |
| controller.image.tag | string | "" |
Controller Manager image tag (defaults to chart appVersion if not set) |
| controller.imagePullSecrets | list | [] |
Image pull secrets for Controller Manager |
| controller.livenessProbe | object | {"httpGet":{"path":"/healthz","port":8081},"initialDelaySeconds":15,"periodSeconds":20} |
Liveness probe configuration |
| controller.metrics.certManager.certKey | string | "tls.key" |
Certificate key file name |
| controller.metrics.certManager.certName | string | "tls.crt" |
Certificate file name |
| controller.metrics.certManager.certPath | string | "/tmp/k8s-metrics-server/metrics-certs" |
Path to mount certificates |
| controller.metrics.certManager.enabled | bool | false |
Enable cert-manager for metrics certificates |
| controller.metrics.enabled | bool | false |
Enable metrics service |
| controller.metrics.service.annotations | object | {} |
Metrics service annotations |
| controller.metrics.service.port | int | 8443 |
Metrics service port |
| controller.metrics.service.type | string | "ClusterIP" |
Metrics service type |
| controller.metrics.serviceMonitor.additionalLabels | object | {} |
Additional labels for ServiceMonitor |
| controller.metrics.serviceMonitor.enabled | bool | false |
Enable ServiceMonitor |
| controller.metrics.serviceMonitor.interval | string | "30s" |
Scrape interval |
| controller.metrics.serviceMonitor.scrapeTimeout | string | "10s" |
Scrape timeout |
| controller.nameOverride | string | "" |
Override Controller Manager name |
| controller.nodeSelector | object | {} |
Node selector for pod assignment |
| controller.podAnnotations | object | {} |
Pod annotations |
| controller.podLabels | object | {} |
Pod labels |
| controller.podSecurityContext | object | {"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}} |
Pod security context |
| controller.readinessProbe | object | {"httpGet":{"path":"/readyz","port":8081},"initialDelaySeconds":5,"periodSeconds":10} |
Readiness probe configuration |
| controller.replicaCount | int | 1 |
Number of Controller Manager replicas |
| controller.resources | object | {"limits":{"cpu":"300m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}} |
Resource limits and requests |
| controller.securityContext | object | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} |
Container security context |
| controller.serviceAccount.annotations | object | {} |
Service account annotations |
| controller.serviceAccount.create | bool | true |
Create service account |
| controller.serviceAccount.name | string | "" |
Service account name (auto-generated if not set) |
| controller.tolerations | list | [] |
Tolerations for pod assignment |
| createNamespace | bool | false |
Create namespace if it doesn't exist |
| etcd.affinity | object | {} |
Affinity for pod assignment |
| etcd.args.advertiseClientUrls | string | "http://localhost:2379" |
Advertise client URLs |
| etcd.args.dataDir | string | "/etcd-data-dir/default.etcd" |
Data directory |
| etcd.args.listenClientUrls | string | "http://[::]:2379" |
Listen client URLs |
| etcd.enabled | bool | true |
Enable etcd deployment |
| etcd.extraArgs | object | {} |
Additional command-line arguments as key-value pairs |
| etcd.extraEnv | list | [] |
Additional environment variables |
| etcd.image.pullPolicy | string | "IfNotPresent" |
Image pull policy |
| etcd.image.repository | string | "quay.io/coreos/etcd" |
etcd image repository |
| etcd.image.tag | string | "v3.6.11" |
etcd image tag |
| etcd.imagePullSecrets | list | [] |
Image pull secrets for etcd |
| etcd.livenessProbe | object | {"httpGet":{"path":"/health","port":2379},"initialDelaySeconds":15,"periodSeconds":20} |
Liveness probe configuration |
| etcd.nodeSelector | object | {} |
Node selector for pod assignment |
| etcd.persistence.accessMode | string | "ReadWriteOnce" |
Access mode |
| etcd.persistence.annotations | object | {} |
PVC annotations |
| etcd.persistence.enabled | bool | true |
Enable persistence |
| etcd.persistence.size | string | "1Gi" |
Storage size |
| etcd.persistence.storageClass | string | "" |
Storage class (uses default if empty) |
| etcd.podAnnotations | object | {} |
Pod annotations |
| etcd.podLabels | object | {} |
Pod labels |
| etcd.podSecurityContext | object | {"seccompProfile":{"type":"RuntimeDefault"}} |
Pod security context |
| etcd.readinessProbe | object | {"httpGet":{"path":"/health","port":2379},"initialDelaySeconds":5,"periodSeconds":10} |
Readiness probe configuration |
| etcd.replicaCount | int | 1 |
Number of etcd replicas (single instance for non-HA) |
| etcd.resources | object | {"limits":{"cpu":"500m","memory":"256Mi"},"requests":{"cpu":"10m","memory":"64Mi"}} |
Resource limits and requests |
| etcd.securityContext | object | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} |
Container security context |
| etcd.service.annotations | object | {} |
Service annotations |
| etcd.service.port | int | 2379 |
Service port |
| etcd.service.type | string | "ClusterIP" |
Service type |
| etcd.tolerations | list | [] |
Tolerations for pod assignment |
| fullnameOverride | string | "" |
Override the full name of the chart |
| global | object | {"imagePullSecrets":[],"storageClass":""} |
Global settings that can be shared with subcharts |
| global.imagePullSecrets | list | [] |
Global image pull secrets |
| global.storageClass | string | "" |
Global storage class for persistent volumes |
| nameOverride | string | "" |
Override the name of the chart |
| namespaceOverride | string | "" |
Override the namespace to install into |
| rbac.additionalAPIServerRules | list | [] |
Additional ClusterRole rules for apiserver |
| rbac.additionalControllerRules | list | [] |
Additional ClusterRole rules for controller |
| rbac.create | bool | true |
Create RBAC resources |
| renderer.caConfigMap | string | "" |
ConfigMap name containing CA bundle for registry connections (e.g., trust-manager's root-bundle) |
| renderer.command | string | "" |
Command to execute in the solar-renderer job |
| renderer.extraArgs | list | [] |
Additional args for the renderer |
| renderer.image.repository | string | "ghcr.io/opendefensecloud/solar-renderer" |
|
| renderer.image.tag | string | "" |
|
| renderer.imagePullSecrets | list | [] |
Image pull secrets for the renderer Pod. Use the Kubernetes shape [{name: my-secret}] (matches apiserver.imagePullSecrets etc.). Each referenced Secret must exist (type kubernetes.io/dockerconfigjson) in every namespace where Targets/RenderTasks are created — the renderer Pod runs in the RenderTask's namespace, so cross-namespace references don't work. Merged with global.imagePullSecrets. See the chart README for the recommended External Secrets Operator pattern that distributes a single source-of-truth credential to every namespace. |
| ui.affinity | object | {} |
Affinity for pod assignment |
| ui.args.authMode | string | "token" |
How the user's OIDC identity reaches the Kubernetes API: "token" forwards the id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user (and grants it impersonate RBAC). Only rendered when oidc.issuer is set; "impersonate" without an issuer is rejected at template time. |
| ui.command | list | ["/solar-ui"] |
Command to run in the container |
| ui.enabled | bool | false |
Enable UI deployment. Off by default: it usually needs an OIDC issuer to authenticate against, which is site-specific. |
| ui.extraArgs | object | {} |
Additional command-line arguments as key-value pairs |
| ui.extraEnv | list | [] |
Additional environment variables |
| ui.image.pullPolicy | string | "IfNotPresent" |
Image pull policy |
| ui.image.repository | string | "ghcr.io/opendefensecloud/solar-ui" |
UI image repository |
| ui.image.tag | string | "" |
UI image tag (defaults to chart appVersion if not set) |
| ui.imagePullSecrets | list | [] |
Image pull secrets for UI |
| ui.livenessProbe | object | {"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":15,"periodSeconds":20} |
Liveness probe configuration |
| ui.nodeSelector | object | {} |
Node selector for pod assignment |
| ui.oidc.clientID | string | "solar-ui" |
OIDC client ID |
| ui.oidc.existingSecret | string | "" |
Name of an existing Secret holding the OIDC client secret. Left empty for public clients (no client secret). |
| ui.oidc.existingSecretKey | string | "client-secret" |
Key within existingSecret holding the client secret |
| ui.oidc.issuer | string | "" |
OIDC issuer URL, e.g. https://dex.example.com. Leave empty to run the UI without authentication: no --oidc-* arguments are rendered, the BFF falls back to its no-op provider and every API call runs as the UI's own ServiceAccount. Dev and testing only, don't expose it. |
| ui.oidc.redirectURL | string | "" |
OIDC redirect URL — the externally reachable /api/auth/callback of this UI, e.g. https://solar.example.com/api/auth/callback. Required when issuer is set. |
| ui.podAnnotations | object | {} |
Pod annotations |
| ui.podLabels | object | {} |
Pod labels |
| ui.podSecurityContext | object | {"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}} |
Pod security context |
| ui.readinessProbe | object | {"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":5,"periodSeconds":10} |
Readiness probe configuration |
| ui.replicaCount | int | 1 |
Number of UI replicas. Keep at 1 unless a session key is pinned: the BFF generates one per process, so a second replica cannot read the first one's session cookies. |
| ui.resources | object | {"limits":{"cpu":"300m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}} |
Resource limits and requests |
| ui.securityContext | object | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}} |
Container security context |
| ui.service.annotations | object | {} |
UI service annotations |
| ui.service.port | int | 8090 |
UI service port. Also the port the BFF listens on. |
| ui.service.type | string | "ClusterIP" |
UI service type |
| ui.serviceAccount.annotations | object | {} |
Service account annotations |
| ui.serviceAccount.create | bool | true |
Create service account |
| ui.serviceAccount.name | string | "" |
Service account name (auto-generated if not set) |
| ui.tolerations | list | [] |
Tolerations for pod assignment |
Contributing¶
Contributions are welcome! Please see the Contributing Guide.
Maintainers¶
| Name | Url | |
|---|---|---|
| ODD Team | odd@opendefense.cloud |
License¶
Apache-2.0