helm: manage vector engines and provision vector buckets (#12518)

* helm: deploy vector engines with TLS, probes, and monitoring

* helm: provision vector buckets through the S3 Vectors API

* helm: bracket IPv6 pod addresses for vector listeners

* helm: discover vector engines through the all-in-one master

* helm: accept vector component NetworkPolicy overrides

* helm: bind vector metrics to the pod address on both IP families

* helm: provision vector buckets through embedded and all-in-one gateways

* docs: use the full Helm name for external vector certificates

* helm: render vector instance labels as strings

* helm: allow vector bucket hooks to use restricted security contexts

* helm: honor vector security context enable switches

* helm: prevent duplicate vector monitoring labels

* helm: reject metrics flags with the default pre-metrics vector image

* test: skip vector chart compatibility checks when Helm is unavailable
This commit is contained in:
Chris Lu authored and GitHub committed 2026-11-09 16:34:57 -08:00
1 parent c1afb1b1cb
commit 5abb18e678
14 files changed
+826 -2

No files matched your search

+7
View File
@@ -37,6 +37,13 @@ jobs:
with:
version: v3.18.4
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Test vector chart resources
run: go test ./k8s/charts/seaweedfs/tests -count=1
- uses: actions/setup-python@v7
with:
python-version: '3.10'
+1 -1
View File
@@ -3,4 +3,4 @@ description: SeaweedFS
name: seaweedfs
appVersion: "4.53"
# Dev note: Trigger a helm chart release by `git tag -a helm-<version>`
version: 4.53.0
version: 4.53.1
+44
View File
@@ -687,3 +687,47 @@ generates a new UUID and the license stops matching. See
[Master data](#master-data-hostpath-vs-a-claim). Check the binding with
`kubectl exec <master-pod> -- curl -s localhost:9333/license/status`
(`cluster_uuid` must equal `license_uuid`).
## Vector buckets
The S3 gateway serves the S3 Vectors API on its existing endpoint. Enable a vector engine and Lance maintenance worker alongside the cluster:
```yaml
s3:
enabled: true
vector:
enabled: true
replicas: 1
worker:
enabled: true
admin:
enabled: true
secret:
adminPassword: change-me
global:
seaweedfs:
securityConfig:
jwtSigning:
filerWrite: true
```
The engine registers its pod address with the masters and stores indexes through the filers and volume servers. Both Go and Rust volume servers use the same setup. The maintenance worker builds and updates ANN indexes; queries can scan data before indexing completes. Enabling filer write signing also requires clients writing directly to the filer HTTP API to supply a signed JWT.
The bundled engine images support Linux amd64 and arm64. Use `vector.imageOverride` to select a compatible engine build, `vector.masters` for an external cluster, and `vector.resources` to set memory and CPU limits. The cache defaults to 512 MiB and is configured with `vector.readCacheMB`.
`global.seaweedfs.enableSecurity` provisions and mounts the vector certificate and shared security configuration. With external certificates, provide the `<fullname>-vector-cert` secret (`my-seaweedfs-vector-cert` for release `my`). `fullname` follows `seaweedfs.fullname`, including `fullnameOverride`, `nameOverride`, and DNS-name truncation. Provide this secret alongside the existing CA and client secrets.
The default `vector.metricsPort: 0` and TCP probes support existing engine images. The default 4.53 image does not accept metrics flags; enabling metrics with it is rejected at render time. For engine builds with `/metrics`, `/health`, and `/ready`, set `vector.imageOverride` to that compatible image and `vector.metricsPort: 9329`; readiness then checks master registration. Enabling `global.seaweedfs.monitoring.enabled` also creates the vector ServiceMonitor. NetworkPolicy includes the configured gRPC and metrics ports.
To provision vector buckets during installation and upgrades, set:
```yaml
vector:
enabled: true
createBuckets:
- embeddings
```
The hook uses the AWS CLI's S3 Vectors API and works with existing SeaweedFS vector releases. It creates missing buckets, preserves existing vector buckets, and fails if the name belongs to an Iceberg bucket. It uses the chart's generated S3 admin credentials, including `s3.credentials.admin.existingSecret`. With a custom `s3.existingConfigSecret` or an external endpoint, set `vector.bucketHook.credentialsSecret` to a Secret containing `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (and optionally `AWS_SESSION_TOKEN`). Key names are configurable. `vector.bucketHook.endpoint` overrides the internal S3 service URL; `caBundleSecret` and `caBundleKey` configure a private HTTPS CA. The hook image and resource limits are configurable under `vector.bucketHook`. For restricted Pod Security, select a non-root-compatible image and configure `vector.bucketHook.podSecurityContext` and `containerSecurityContext`, including `runAsNonRoot` and the image user ID.
@@ -0,0 +1,50 @@
{{- include "seaweedfs.compat" . -}}
{{- if and .Values.vector.enabled .Values.global.seaweedfs.enableSecurity (not .Values.certificates.externalCertificates.enabled)}}
apiVersion: cert-manager.io/v1{{ if .Values.global.seaweedfs.certificates.alphacrds }}alpha1{{ end }}
kind: Certificate
metadata:
name: {{ include "seaweedfs.fullname" . }}-vector-cert
namespace: {{ include "seaweedfs.namespace" . | quote }}
labels:
app.kubernetes.io/name: {{ template "seaweedfs.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/component: vector
{{- with include "seaweedfs.commonLabels" $ }}{{ . | nindent 4 }}{{ end }}
{{- if .Values.vector.annotations }}
annotations:
{{- toYaml .Values.vector.annotations | nindent 4 }}
{{- end }}
spec:
secretName: {{ include "seaweedfs.fullname" . }}-vector-cert
issuerRef:
name: {{ include "seaweedfs.fullname" . }}-ca-issuer
kind: Issuer
commonName: {{ .Values.certificates.commonName }}
subject:
organizations:
- "SeaweedFS CA"
dnsNames:
- '*.{{ include "seaweedfs.fullname" . }}-vector'
- '*.{{ include "seaweedfs.fullname" . }}-vector.{{ include "seaweedfs.namespace" . }}'
- '*.{{ include "seaweedfs.fullname" . }}-vector.{{ include "seaweedfs.namespace" . }}.svc'
- '*.{{ include "seaweedfs.fullname" . }}-vector.{{ include "seaweedfs.namespace" . }}.svc.cluster.local'
{{- if .Values.certificates.dnsNames }}
{{- range .Values.certificates.dnsNames }}
- {{ . | quote }}
{{- end }}
{{- end }}
{{- if .Values.certificates.ipAddresses }}
ipAddresses:
{{- range .Values.certificates.ipAddresses }}
- {{ . }}
{{- end }}
{{- end }}
privateKey:
algorithm: {{ .Values.certificates.keyAlgorithm }}
size: {{ .Values.certificates.keySize }}
duration: {{ .Values.certificates.duration }}
renewBefore: {{ .Values.certificates.renewBefore }}
{{- end }}
@@ -770,3 +770,15 @@ on install.
{{- $context := merge (dict "Values" (omit .Values "commonLabels")) (omit . "Values") -}}
{{- include (print .Template.BasePath "/master/master-configmap.yaml") $context | sha256sum -}}
{{- end -}}
{{- define "seaweedfs.vector.selectorLabels" -}}
app.kubernetes.io/name: {{ include "seaweedfs.name" . }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/component: vector
{{- end -}}
{{- define "seaweedfs.vector.labels" -}}
{{ include "seaweedfs.vector.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- with include "seaweedfs.commonLabels" . }}{{ . | nindent 0 }}{{ end }}
{{- end -}}
@@ -110,6 +110,18 @@
{{- $targets = append $targets (dict "component" "worker" "ports" $ports) }}
{{- end }}
{{- if .Values.vector.enabled }}
{{- $ports := list .Values.vector.port }}
{{- if .Values.vector.metricsPort }}
{{- $ports = append $ports .Values.vector.metricsPort }}
{{- end }}
{{- $targets = append $targets (dict "component" "vector" "ports" $ports) }}
{{- end }}
{{- if and .Values.vector.enabled .Values.vector.createBuckets }}
{{- $targets = append $targets (dict "component" "vector-bucket-hook" "ports" (list)) }}
{{- end }}
{{- if .Values.cosi.enabled }}
{{- /* The provisioner sidecar watches the COSI CRs, so it needs the API
server; nothing needs to reach the driver itself. */}}
@@ -143,7 +155,7 @@
every component the chart can produce rather than the ones enabled here,
so one values file can still carry overrides for a component this release
leaves off. */}}
{{- $known := list "master" "filer" "s3" "sftp" "admin" "worker" "volume" "bucket-hook" "volume-resize-hook" "objectstorage-provisioner" "seaweedfs-all-in-one" }}
{{- $known := list "vector" "vector-bucket-hook" "master" "filer" "s3" "sftp" "admin" "worker" "volume" "bucket-hook" "volume-resize-hook" "objectstorage-provisioner" "seaweedfs-all-in-one" }}
{{- range $vname, $_ := (.Values.volumes | default dict) }}
{{- $known = append $known (trimSuffix "-" (printf "volume-%s" $vname)) }}
{{- end }}
@@ -94,6 +94,10 @@ data:
cert = "/usr/local/share/ca-certificates/admin/tls.crt"
key = "/usr/local/share/ca-certificates/admin/tls.key"
[grpc.vector]
cert = "/usr/local/share/ca-certificates/vector/tls.crt"
key = "/usr/local/share/ca-certificates/vector/tls.key"
[grpc.worker]
cert = "/usr/local/share/ca-certificates/worker/tls.crt"
key = "/usr/local/share/ca-certificates/worker/tls.key"
@@ -0,0 +1,167 @@
{{- include "seaweedfs.compat" . -}}
{{- if and .Values.vector.enabled .Values.vector.createBuckets }}
{{- $hook := .Values.vector.bucketHook }}
{{- $component := "s3" }}
{{- $port := .Values.s3.port }}
{{- $enabled := .Values.s3.enabled }}
{{- $auth := .Values.s3.enableAuth }}
{{- $configSecret := .Values.s3.existingConfigSecret }}
{{- if and .Values.allInOne.enabled .Values.allInOne.s3.enabled }}
{{- $component = "all-in-one" }}
{{- $port = .Values.allInOne.s3.port | default .Values.s3.port }}
{{- $enabled = true }}
{{- $auth = or .Values.allInOne.s3.enableAuth .Values.s3.enableAuth .Values.filer.s3.enableAuth }}
{{- $configSecret = or .Values.allInOne.s3.existingConfigSecret .Values.s3.existingConfigSecret .Values.filer.s3.existingConfigSecret }}
{{- else if and (not $enabled) .Values.filer.s3.enabled }}
{{- $component = "filer" }}
{{- $port = .Values.filer.s3.port }}
{{- $enabled = true }}
{{- $auth = .Values.filer.s3.enableAuth }}
{{- $configSecret = .Values.filer.s3.existingConfigSecret }}
{{- end }}
{{- if and (not $enabled) (not $hook.endpoint) }}
{{- fail "vector.createBuckets requires an enabled S3 gateway or vector.bucketHook.endpoint" }}
{{- end }}
{{- range .Values.vector.createBuckets }}
{{- if not (kindIs "string" .) }}{{ fail "vector.createBuckets entries must be bucket names" }}{{ end }}
{{- if not (and (ge (len .) 3) (le (len .) 63) (regexMatch "^[a-z0-9][a-z0-9-]*[a-z0-9]$" .)) }}
{{- fail "vector.createBuckets names must be 3-63 lowercase letters, digits or hyphens" }}
{{- end }}
{{- end }}
{{- $credentials := .Values.s3.credentials | default dict }}
{{- $admin := $credentials.admin | default dict }}
{{- $secret := $hook.credentialsSecret }}
{{- $accessKey := $hook.accessKeyKey }}
{{- $secretKey := $hook.secretKeyKey }}
{{- if and $auth (not $secret) }}
{{- if $configSecret }}{{ fail "vector.bucketHook.credentialsSecret is required with s3.existingConfigSecret" }}{{ end }}
{{- $secret = $admin.existingSecret | default (printf "%s-s3-secret" (include "seaweedfs.fullname" .)) }}
{{- $accessKey = $admin.accessKeyKey | default "admin_access_key_id" }}
{{- $secretKey = $admin.secretKeyKey | default "admin_secret_access_key" }}
{{- end }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "seaweedfs.fullname" . }}-vector-bucket-hook
namespace: {{ include "seaweedfs.namespace" . | quote }}
labels:
app.kubernetes.io/name: {{ include "seaweedfs.name" . }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/component: vector-bucket-hook
{{- with include "seaweedfs.commonLabels" . }}{{ . | nindent 4 }}{{ end }}
annotations:
helm.sh/hook: post-install,post-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
backoffLimit: 2
activeDeadlineSeconds: 360
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "seaweedfs.name" . }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
app.kubernetes.io/component: vector-bucket-hook
spec:
restartPolicy: Never
automountServiceAccountToken: false
{{- with $hook.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.vector.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.vector.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "seaweedfs.imagePullSecrets" . | nindent 6 }}
containers:
- name: create-vector-buckets
image: {{ $hook.image | quote }}
command: ["/bin/sh", "-ec"]
args:
- |
for bucket in {{ range .Values.vector.createBuckets }}{{ . | quote }} {{ end }}; do
attempt=0
while ! aws --endpoint-url "$S3_ENDPOINT" s3vectors get-vector-bucket --vector-bucket-name "$bucket" >/dev/null 2>/tmp/bucket-error; do
if grep -q NotFound /tmp/bucket-error; then
aws --endpoint-url "$S3_ENDPOINT" s3vectors create-vector-bucket --vector-bucket-name "$bucket" ||
aws --endpoint-url "$S3_ENDPOINT" s3vectors get-vector-bucket --vector-bucket-name "$bucket"
break
fi
if grep -q AccessDenied /tmp/bucket-error; then cat /tmp/bucket-error >&2; exit 1; fi
attempt=$((attempt + 1))
if [ "$attempt" -ge 60 ]; then cat /tmp/bucket-error >&2; exit 1; fi
sleep 5
done
done
env:
- name: S3_ENDPOINT
value: {{ $hook.endpoint | default (printf "http://%s.%s:%d" (include "seaweedfs.componentName" (list . $component)) (include "seaweedfs.namespace" .) (int $port)) | quote }}
- name: AWS_DEFAULT_REGION
value: {{ $hook.region | quote }}
- name: AWS_EC2_METADATA_DISABLED
value: "true"
- name: AWS_PAGER
value: ""
- name: AWS_MAX_ATTEMPTS
value: "2"
- name: AWS_ACCESS_KEY_ID
{{- if $secret }}
valueFrom:
secretKeyRef:
name: {{ $secret | quote }}
key: {{ $accessKey | quote }}
{{- else }}
value: seaweedfs
{{- end }}
- name: AWS_SECRET_ACCESS_KEY
{{- if $secret }}
valueFrom:
secretKeyRef:
name: {{ $secret | quote }}
key: {{ $secretKey | quote }}
{{- else }}
value: seaweedfs
{{- end }}
{{- if $hook.credentialsSecret }}
- name: AWS_SESSION_TOKEN
valueFrom:
secretKeyRef:
name: {{ $secret | quote }}
key: {{ $hook.sessionTokenKey | quote }}
optional: true
{{- end }}
{{- if $hook.caBundleSecret }}
- name: AWS_CA_BUNDLE
value: /etc/ssl/vector/ca.crt
{{- end }}
securityContext:
{{- toYaml $hook.containerSecurityContext | nindent 12 }}
{{- with $hook.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: tmp
mountPath: /tmp
{{- if $hook.caBundleSecret }}
- name: ca
mountPath: /etc/ssl/vector
readOnly: true
{{- end }}
volumes:
- name: tmp
emptyDir: {}
{{- if $hook.caBundleSecret }}
- name: ca
secret:
secretName: {{ $hook.caBundleSecret | quote }}
items:
- key: {{ $hook.caBundleKey | quote }}
path: ca.crt
{{- end }}
{{- end }}
@@ -0,0 +1,166 @@
{{- include "seaweedfs.compat" . -}}
{{- if .Values.vector.enabled }}
{{- if and .Values.vector.metricsPort (not .Values.vector.imageOverride) (eq (toString (.Values.image.tag | default .Chart.AppVersion)) "4.53") }}
{{- fail "vector.metricsPort requires vector.imageOverride selecting an engine with metrics support; the default 4.53 image does not support metrics" }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "seaweedfs.componentName" (list . "vector") }}
namespace: {{ include "seaweedfs.namespace" . | quote }}
labels:
{{- include "seaweedfs.vector.labels" . | nindent 4 }}
{{- with .Values.vector.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.vector.replicas }}
selector:
matchLabels:
{{- include "seaweedfs.vector.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "seaweedfs.vector.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}{{ toYaml . | nindent 8 }}{{ end }}
{{- with .Values.vector.podLabels }}{{ toYaml . | nindent 8 }}{{ end }}
annotations:
{{- with .Values.podAnnotations }}{{ toYaml . | nindent 8 }}{{ end }}
{{- with .Values.vector.podAnnotations }}{{ toYaml . | nindent 8 }}{{ end }}
spec:
{{- include "seaweedfs.imagePullSecrets" . | nindent 6 }}
automountServiceAccountToken: {{ .Values.global.seaweedfs.automountServiceAccountToken }}
{{- with .Values.vector.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.vector.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.vector.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if and .Values.vector.podSecurityContext (or (not (hasKey .Values.vector.podSecurityContext "enabled")) .Values.vector.podSecurityContext.enabled) }}
securityContext:
{{- omit .Values.vector.podSecurityContext "enabled" | toYaml | nindent 8 }}
{{- end }}
containers:
- name: vector
image: {{ .Values.vector.imageOverride | default (include "seaweedfs.image" .) | quote }}
imagePullPolicy: {{ .Values.global.seaweedfs.imagePullPolicy }}
{{- $masters := include "seaweedfs.masterServerArg" . }}
{{- if .Values.allInOne.enabled }}{{ $masters = include "seaweedfs.cluster.masterAddress" . }}{{ end }}
command: ["/bin/sh", "-ec"]
args:
- 'case "$POD_IP" in *:*) POD_IP="[$POD_IP]";; esac; exec /usr/bin/weed-vector --listen="$POD_IP:{{ .Values.vector.port }}" "$@"'
- vector
- {{ printf "--masters=%s" (.Values.vector.masters | default $masters) | quote }}
- {{ printf "--read-cache-mb=%d" (int .Values.vector.readCacheMB) | quote }}
{{- if .Values.filer.filerGroup }}
- {{ printf "--filer-group=%s" .Values.filer.filerGroup | quote }}
{{- end }}
{{- if .Values.vector.nprobes }}
- {{ printf "--nprobes=%d" (int .Values.vector.nprobes) | quote }}
{{- end }}
{{- if include "seaweedfs.securityConfigEnabled" . }}
- --security-toml=/etc/seaweedfs/security.toml
{{- end }}
{{- if .Values.vector.metricsPort }}
- {{ printf "--metrics-port=%d" (int .Values.vector.metricsPort) | quote }}
- --metrics-ip=$(POD_IP)
{{- end }}
{{- range .Values.vector.extraArgs }}
- {{ . | quote }}
{{- end }}
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
{{- $env := dict }}
{{- include "seaweedfs.mergeExtraEnvironmentVars" (dict "global" .Values.global.seaweedfs "component" .Values.vector "target" $env) }}
{{- range $key := keys $env | sortAlpha }}
- name: {{ $key }}
{{- $value := index $env $key }}
{{- if kindIs "map" $value }}
valueFrom:
{{- toYaml $value | nindent 16 }}
{{- else }}
value: {{ tpl (toString $value) $ | quote }}
{{- end }}
{{- end }}
ports:
- name: grpc
containerPort: {{ .Values.vector.port }}
{{- if .Values.vector.metricsPort }}
- name: metrics
containerPort: {{ .Values.vector.metricsPort }}
{{- end }}
startupProbe:
tcpSocket:
port: grpc
periodSeconds: 5
failureThreshold: 60
readinessProbe:
{{- if .Values.vector.metricsPort }}
httpGet:
path: /ready
port: metrics
{{- else }}
tcpSocket:
port: grpc
{{- end }}
periodSeconds: 5
livenessProbe:
{{- if .Values.vector.metricsPort }}
httpGet:
path: /health
port: metrics
{{- else }}
tcpSocket:
port: grpc
{{- end }}
initialDelaySeconds: 10
periodSeconds: 10
{{- if or (not (hasKey .Values.vector.containerSecurityContext "enabled")) .Values.vector.containerSecurityContext.enabled }}
securityContext:
{{- omit .Values.vector.containerSecurityContext "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- with .Values.vector.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: tmp
mountPath: /tmp
{{- if include "seaweedfs.securityConfigEnabled" . }}
- name: security-config
mountPath: /etc/seaweedfs
readOnly: true
{{- end }}
{{- if .Values.global.seaweedfs.enableSecurity }}
{{- range list "ca" "client" "vector" }}
- name: {{ . }}-cert
mountPath: /usr/local/share/ca-certificates/{{ . }}
readOnly: true
{{- end }}
{{- end }}
volumes:
- name: tmp
emptyDir: {}
{{- if include "seaweedfs.securityConfigEnabled" . }}
- name: security-config
configMap:
name: {{ include "seaweedfs.fullname" . }}-security-config
{{- end }}
{{- if .Values.global.seaweedfs.enableSecurity }}
{{- range list "ca" "client" "vector" }}
- name: {{ . }}-cert
secret:
secretName: {{ include "seaweedfs.fullname" $ }}-{{ . }}-cert
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,22 @@
{{- if .Values.vector.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "seaweedfs.componentName" (list . "vector") }}
namespace: {{ include "seaweedfs.namespace" . | quote }}
labels:
{{- include "seaweedfs.vector.labels" . | nindent 4 }}
spec:
clusterIP: None
selector:
{{- include "seaweedfs.vector.selectorLabels" . | nindent 4 }}
ports:
- name: grpc
port: {{ .Values.vector.port }}
targetPort: grpc
{{- if .Values.vector.metricsPort }}
- name: metrics
port: {{ .Values.vector.metricsPort }}
targetPort: metrics
{{- end }}
{{- end }}
@@ -0,0 +1,25 @@
{{- if and .Values.vector.enabled .Values.vector.metricsPort .Values.global.seaweedfs.monitoring.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "seaweedfs.componentName" (list . "vector") }}
namespace: {{ include "seaweedfs.namespace" . | quote }}
labels:
{{- include "seaweedfs.vector.selectorLabels" . | nindent 4 }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- with include "seaweedfs.commonLabels" (dict "context" $ "exclude" $.Values.global.seaweedfs.monitoring.additionalLabels) }}{{ . | nindent 4 }}{{ end }}
{{- with .Values.global.seaweedfs.monitoring.additionalLabels }}{{ toYaml . | nindent 4 }}{{ end }}
{{- with .Values.vector.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
endpoints:
- port: metrics
interval: 30s
scrapeTimeout: 5s
selector:
matchLabels:
{{- include "seaweedfs.vector.selectorLabels" . | nindent 6 }}
{{- end }}
@@ -0,0 +1,113 @@
package tests
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestVectorBucketHook(t *testing.T) {
for _, auth := range []bool{false, true} {
t.Run(fmt.Sprint(auth), func(t *testing.T) {
output, documents := render(t, "s3.enabled=true", "vector.enabled=true", "vector.createBuckets[0]=embeddings", "s3.enableAuth="+fmt.Sprint(auth), "networkPolicy.enabled=true")
var found bool
for _, document := range documents {
metadata := document["metadata"].(map[string]interface{})
if document["kind"] == "Job" && strings.HasSuffix(metadata["name"].(string), "-vector-bucket-hook") {
found = true
if metadata["annotations"].(map[string]interface{})["helm.sh/hook"] != "post-install,post-upgrade" {
t.Fatal("missing upgrade hook")
}
}
}
if !found || !strings.Contains(output, "s3vectors get-vector-bucket") || !strings.Contains(output, "s3vectors create-vector-bucket") {
t.Fatal("missing vector bucket provisioning")
}
if auth && !strings.Contains(output, "key: \"admin_access_key_id\"") {
t.Fatal("missing generated admin credential reference")
}
})
}
}
func TestVectorBucketHookRetriesRacesAndRefusesConflicts(t *testing.T) {
_, documents := render(t, "s3.enabled=true", "vector.enabled=true", "vector.createBuckets[0]=embeddings")
var script string
for _, document := range documents {
if document["kind"] != "Job" {
continue
}
metadata := document["metadata"].(map[string]interface{})
if strings.HasSuffix(metadata["name"].(string), "-vector-bucket-hook") {
pod := document["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})
container := pod["containers"].([]interface{})[0].(map[string]interface{})
script = container["args"].([]interface{})[0].(string)
}
}
if script == "" {
t.Fatal("missing hook script")
}
for _, mode := range []string{"missing", "existing", "race", "iceberg", "denied"} {
t.Run(mode, func(t *testing.T) {
dir := t.TempDir()
stub := `#!/bin/sh
shift 2
operation=$2
case "$operation" in
get-vector-bucket)
if [ "$BUCKET_MODE" = denied ]; then echo AccessDenied >&2; exit 1; fi
if [ "$BUCKET_MODE" = existing ] || { [ -f "$BUCKET_STATE" ] && [ "$BUCKET_MODE" != iceberg ]; }; then exit 0; fi
echo NotFoundException >&2; exit 1;;
create-vector-bucket)
touch "$BUCKET_STATE"
if [ "$BUCKET_MODE" = race ] || [ "$BUCKET_MODE" = iceberg ]; then exit 1; fi;;
esac
`
if err := os.WriteFile(filepath.Join(dir, "aws"), []byte(stub), 0755); err != nil {
t.Fatal(err)
}
command := exec.Command("sh", "-ec", script)
command.Env = append(os.Environ(), "PATH="+dir+":"+os.Getenv("PATH"), "S3_ENDPOINT=http://unused", "BUCKET_MODE="+mode, "BUCKET_STATE="+filepath.Join(dir, "state"))
output, err := command.CombinedOutput()
wantFailure := mode == "iceberg" || mode == "denied"
if (err != nil) != wantFailure {
t.Fatalf("mode %s: %v %s", mode, err, output)
}
})
}
}
func TestVectorBucketHookRejectsShellNames(t *testing.T) {
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm is not installed")
}
command := exec.Command("helm", "template", "test", "..", "--set", "s3.enabled=true", "--set", "vector.enabled=true", "--set", "vector.createBuckets[0]=bucket;touch /tmp/invalid")
output, err := command.CombinedOutput()
if err == nil || !strings.Contains(string(output), "vector.createBuckets names") {
t.Fatalf("invalid bucket accepted: %v %s", err, output)
}
}
func TestVectorBucketHookGatewayModes(t *testing.T) {
for _, mode := range []string{"s3", "filer", "allInOne"} {
settings := []string{"vector.enabled=true", "vector.createBuckets[0]=embeddings"}
component := "s3"
switch mode {
case "s3":
settings = append(settings, "s3.enabled=true", "s3.enableAuth=true")
case "filer":
settings = append(settings, "s3.enabled=false", "filer.s3.enabled=true", "filer.s3.enableAuth=true")
component = "filer"
case "allInOne":
settings = append(settings, "s3.enabled=false", "allInOne.enabled=true", "allInOne.s3.enabled=true", "allInOne.s3.enableAuth=true")
component = "all-in-one"
}
output, _ := render(t, settings...)
if !strings.Contains(output, "http://vector-test-seaweedfs-"+component+".default:8333") || !strings.Contains(output, "key: \"admin_access_key_id\"") {
t.Fatalf("missing endpoint or auth for %s", mode)
}
}
}
+148
View File
@@ -0,0 +1,148 @@
package tests
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"testing"
"go.yaml.in/yaml/v3"
)
func render(t *testing.T, settings ...string) (string, []map[string]interface{}) {
t.Helper()
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm is not installed")
}
args := []string{"template", "vector-test", ".."}
for _, setting := range settings {
args = append(args, "--set", setting)
}
output, err := exec.Command("helm", args...).CombinedOutput()
if err != nil {
t.Fatalf("helm: %v\n%s", err, output)
}
decoder := yaml.NewDecoder(strings.NewReader(string(output)))
var documents []map[string]interface{}
for {
var document map[string]interface{}
if err := decoder.Decode(&document); err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
if document != nil {
documents = append(documents, document)
}
}
return string(output), documents
}
func TestVectorDeployment(t *testing.T) {
_, defaults := render(t)
for _, document := range defaults {
if strings.HasSuffix(fmt.Sprint(document["metadata"].(map[string]interface{})["name"]), "-vector") {
t.Fatal("vector resource enabled by default")
}
}
for _, secure := range []bool{false, true} {
t.Run(fmt.Sprintf("security-%v", secure), func(t *testing.T) {
output, documents := render(t, "vector.enabled=true", "vector.metricsPort=9329", "vector.imageOverride=example/metrics-engine:test", "vector.port=8098", "global.seaweedfs.enableSecurity="+fmt.Sprint(secure), "global.seaweedfs.monitoring.enabled=true", "networkPolicy.enabled=true")
for _, want := range []string{"--listen=\"$POD_IP:8098\"", "--metrics-port=9329", "path: /ready", "path: /health", "kind: ServiceMonitor"} {
if !strings.Contains(output, want) {
t.Fatalf("missing %s", want)
}
}
found := false
for _, document := range documents {
metadata := document["metadata"].(map[string]interface{})
if document["kind"] == "Deployment" && strings.HasSuffix(metadata["name"].(string), "-vector") {
found = true
spec := document["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})
if spec["nodeSelector"].(map[string]interface{})["kubernetes.io/os"] != "linux" {
t.Fatal("vector engine must use Linux image platforms")
}
}
}
if !found {
t.Fatal("no vector deployment")
}
if secure && (!strings.Contains(output, "[grpc.vector]") || !strings.Contains(output, "vector-cert")) {
t.Fatal("missing vector TLS configuration")
}
})
}
}
func TestVectorExistingImagesAndExternalCertificates(t *testing.T) {
output, _ := render(t, "vector.enabled=true")
start := strings.Index(output, "# Source: seaweedfs/templates/vector/vector-deployment.yaml")
deployment := strings.Split(output[start:], "\n---")[0]
if strings.Contains(deployment, "--metrics-port") || !strings.Contains(deployment, "tcpSocket:") {
t.Fatal("default probes must support existing vector images")
}
output, _ = render(t, "vector.enabled=true", "global.seaweedfs.enableSecurity=true", "certificates.externalCertificates.enabled=true")
if strings.Contains(output, "kind: Certificate") || !strings.Contains(output, "mountPath: /usr/local/share/ca-certificates/vector") {
t.Fatal("external vector certificates are not mounted")
}
}
func TestVectorIPv6Listen(t *testing.T) {
_, docs := render(t, "vector.enabled=true")
var args []interface{}
for _, doc := range docs {
if doc["kind"] != "Deployment" {
continue
}
pod := doc["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})
c := pod["containers"].([]interface{})[0].(map[string]interface{})
if c["name"] == "vector" {
args = c["args"].([]interface{})
}
}
for _, ip := range []string{"10.0.0.20", "fd00::20"} {
dir := t.TempDir()
script := strings.ReplaceAll(args[0].(string), "/usr/bin/weed-vector", "printf '%s\\n'")
cmd := exec.Command("sh", "-ec", script, "vector", "--masters=master")
cmd.Dir = dir
cmd.Env = append(os.Environ(), "POD_IP="+ip)
out, err := cmd.CombinedOutput()
address := ip
if strings.Contains(ip, ":") {
address = "[" + ip + "]"
}
if err != nil || !strings.Contains(string(out), "--listen="+address+":8090") || !strings.Contains(string(out), "--masters=master") {
t.Fatalf("%s: %s %v", ip, out, err)
}
}
}
func TestVectorAllInOneMasters(t *testing.T) {
output, _ := render(t, "vector.enabled=true", "allInOne.enabled=true", "master.enabled=false")
if !strings.Contains(output, "--masters=vector-test-seaweedfs-all-in-one.default:9333") {
t.Fatal("vector cannot discover all-in-one master")
}
output, _ = render(t, "vector.enabled=true", "allInOne.enabled=true", "master.enabled=false", "vector.masters=external:9333")
if !strings.Contains(output, "--masters=external:9333") {
t.Fatal("explicit master override lost")
}
}
func TestVectorNetworkPolicyOverrides(t *testing.T) {
output, _ := render(t, "vector.enabled=true", "s3.enabled=true", "vector.createBuckets[0]=embeddings", "networkPolicy.enabled=true", "networkPolicy.components.vector.extraIngress[0].ports[0].port=9329", "networkPolicy.egress.enabled=true", "networkPolicy.components.vector-bucket-hook.extraEgress[0].ports[0].port=443")
if !strings.Contains(output, "port: 9329") || !strings.Contains(output, "port: 443") {
t.Fatal("vector network overrides missing")
}
}
func TestVectorMetricsImageCompatibility(t *testing.T) {
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm is not installed")
}
out, err := exec.Command("helm", "template", "vector-test", "..", "--set", "vector.enabled=true", "--set", "vector.metricsPort=9329").CombinedOutput()
if err == nil || !strings.Contains(string(out), "default 4.53 image does not support metrics") {
t.Fatalf("incompatible image accepted: %v %s", err, out)
}
render(t, "vector.enabled=true", "vector.metricsPort=9329", "vector.imageOverride=example/metrics-engine:test")
}
+54
View File
@@ -1733,6 +1733,60 @@ admin:
serviceMonitor:
annotations: {}
vector:
enabled: false
imageOverride: null
replicas: 1
port: 8090
metricsPort: 0
masters: ""
readCacheMB: 512
nprobes: null
extraArgs: []
extraEnvironmentVars: {}
podLabels: {}
podAnnotations: {}
annotations: {}
nodeSelector:
kubernetes.io/os: linux
tolerations: []
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: [amd64, arm64]
resources: {}
podSecurityContext: {}
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
serviceMonitor:
annotations: {}
createBuckets: []
bucketHook:
podSecurityContext:
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
image: amazon/aws-cli:2.39.6
endpoint: ""
region: us-east-1
credentialsSecret: ""
accessKeyKey: AWS_ACCESS_KEY_ID
secretKeyKey: AWS_SECRET_ACCESS_KEY
sessionTokenKey: AWS_SESSION_TOKEN
caBundleSecret: ""
caBundleKey: tls.crt
resources: {}
worker:
enabled: false
imageOverride: null