diff --git a/charts/unstructured-api/.helmignore b/charts/unstructured-api/.helmignore new file mode 100644 index 00000000..d8b188a2 --- /dev/null +++ b/charts/unstructured-api/.helmignore @@ -0,0 +1,9 @@ +# Patterns to ignore when building packages. +*.tgz +.git +.gitignore +.idea/ +*.tmproj +.vscode/ +ci/ +.DS_Store diff --git a/charts/unstructured-api/Chart.yaml b/charts/unstructured-api/Chart.yaml new file mode 100644 index 00000000..899a8400 --- /dev/null +++ b/charts/unstructured-api/Chart.yaml @@ -0,0 +1,26 @@ +apiVersion: v2 +name: unstructured-api +description: A Helm chart for the self-hosted Unstructured document-partitioning REST API. +type: application +# Chart version. Bump on any change to the chart itself. +version: 0.1.0 +# The version of the unstructured-api application shipped by default (see values.yaml image.tag). +appVersion: "0.1.10" +kubeVersion: ">=1.23.0-0" +home: https://github.com/Unstructured-IO/unstructured-api +sources: + - https://github.com/Unstructured-IO/unstructured-api +keywords: + - unstructured + - document-processing + - partitioning + - ocr + - nlp + - fastapi +maintainers: + - name: Unstructured API contributors + url: https://github.com/Unstructured-IO/unstructured-api +icon: https://raw.githubusercontent.com/Unstructured-IO/unstructured-api/main/img/unstructured_logo.svg +annotations: + category: MachineLearning + licenses: Apache-2.0 diff --git a/charts/unstructured-api/README.md b/charts/unstructured-api/README.md new file mode 100644 index 00000000..4e082e6c --- /dev/null +++ b/charts/unstructured-api/README.md @@ -0,0 +1,115 @@ +# unstructured-api + +Helm chart for the self-hosted [Unstructured API](https://github.com/Unstructured-IO/unstructured-api), +a REST API that partitions documents with the +[`unstructured`](https://github.com/Unstructured-IO/unstructured) library. + +It deploys a stateless Deployment behind a Service. The image bundles ML models, Tesseract OCR, +and LibreOffice, so it is CPU/memory heavy — size `resources` accordingly for `hi_res` workloads. + +## Quick start + +```bash +helm install unstructured-api ./charts/unstructured-api +kubectl port-forward svc/unstructured-api 8000:80 +curl --fail http://localhost:8000/healthcheck +``` + +Requirements: Kubernetes >= 1.23, Helm >= 3.8, and a node that can fit +`resources.requests` (default `500m` / `3Gi`). + +## Configuration + +Common values. See [`values.yaml`](./values.yaml) for the rest. + +### Image + +| Key | Default | Description | +|---|---|---| +| `image.repository` | `quay.io/unstructured-io/unstructured-api` | Image repository. | +| `image.tag` | `""` | Image tag; defaults to the chart `appVersion`. | +| `image.pullPolicy` | `IfNotPresent` | Pull policy. | +| `imagePullSecrets` | `[]` | Secrets for a private registry. | + +### Workload + +| Key | Default | Description | +|---|---|---| +| `replicaCount` | `1` | Replicas (ignored when autoscaling is on). | +| `resources.requests` | `500m` / `3Gi` | CPU / memory requests. | +| `resources.limits` | `4` / `8Gi` | CPU / memory limits. | +| `terminationGracePeriodSeconds` | `120` | Time to finish in-flight requests on shutdown. | +| `progressDeadlineSeconds` | `900` | Rollout deadline (the image is slow to pull). | +| `podSecurityContext` / `securityContext` | non-root uid/gid `1000`, drop `ALL` | Security contexts. | +| `nodeSelector` / `tolerations` / `affinity` | `{}` | Scheduling. | +| `podAntiAffinity` | `soft` | Spread replicas across nodes: `soft`, `hard`, `""`. | + +### Application (env vars) + +| Key | Default | Env var | Description | +|---|---|---|---| +| `config.port` | `8000` | `PORT` | Listen port. | +| `config.host` | `0.0.0.0` | `HOST` | Bind interface. | +| `config.workers` | `1` | `WORKERS` | Uvicorn workers per pod. | +| `config.memoryFreeMinimumMb` | `2048` | `UNSTRUCTURED_MEMORY_FREE_MINIMUM_MB` | Return 503 below this free memory; `0` disables. | +| `config.allowedOrigins` | `""` | `ALLOWED_ORIGINS` | Comma-separated CORS origins. | +| `config.maxLifetimeSeconds` | `""` | `MAX_LIFETIME_SECONDS` | Graceful self-restart after N seconds. | +| `config.env` | `prod` | `ENV` | Silences the noisy uvicorn error logger. | +| `config.parallelMode.*` | off | `UNSTRUCTURED_PARALLEL_MODE_*` | Experimental parallel PDF mode. | +| `extraEnv` / `extraEnvFrom` | `[]` | | Extra environment variables. | + +### Service, Ingress, autoscaling + +| Key | Default | Description | +|---|---|---| +| `service.enabled` | `true` | Create a Service (required by Ingress and the helm test). | +| `service.type` / `service.port` | `ClusterIP` / `80` | Service type and port. | +| `ingress.enabled` | `false` | Create an Ingress. | +| `autoscaling.enabled` | `false` | Create a HorizontalPodAutoscaler. | +| `autoscaling.minReplicas` / `maxReplicas` | `1` / `5` | HPA bounds. | +| `autoscaling.targetCPUUtilizationPercentage` | `80` | CPU target (% of the CPU request). | +| `podDisruptionBudget.enabled` | `false` | Create a PodDisruptionBudget. | + +## API-key auth + +Set `UNSTRUCTURED_API_KEY` so clients must send the `unstructured-api-key` header. The chart +creates the Secret, or point it at an existing one: + +```yaml +apiKey: + enabled: true + value: "super-secret-key" +# or +apiKey: + enabled: true + existingSecret: my-unstructured-secret + existingSecretKey: UNSTRUCTURED_API_KEY +``` + +## Probes + +All probes hit `GET /healthcheck`. This only confirms the HTTP server is up — it does not check +that models are loaded (they load on the first request) or that memory is free (the 503 guard is +in the partition endpoint). So readiness won't drain an overloaded pod; deeper checks would need +an app-side endpoint. The `startupProbe` covers process start only, not model loading. + +## Scaling + +Each worker loads its own copy of the models, so scale out (`replicaCount` / `autoscaling`) rather +than raising `config.workers`. HPA CPU utilisation is relative to the CPU request, so tune +`targetCPUUtilizationPercentage` and `resources.requests.cpu` together. + +Scale-out isn't instant: the image is ~10GB, so a replica on a node without it cached can't serve +until the pull finishes (minutes). Pre-pull or cache the image, or keep a warm node pool. +`progressDeadlineSeconds: 900` keeps rollouts from failing mid-pull. + +`podAntiAffinity: hard` gives one pod per node, but needs at least `maxReplicas` schedulable nodes +or the extras stay `Pending`. + +## Test + +```bash +helm test my-release +``` + +Runs a Pod that curls `/healthcheck` through the Service. diff --git a/charts/unstructured-api/templates/NOTES.txt b/charts/unstructured-api/templates/NOTES.txt new file mode 100644 index 00000000..93f01c42 --- /dev/null +++ b/charts/unstructured-api/templates/NOTES.txt @@ -0,0 +1,62 @@ +✅ {{ include "unstructured-api.fullname" . }} has been deployed. + +The Unstructured API listens on container port {{ .Values.config.port }}. +{{- if .Values.service.enabled }} +It is exposed by a {{ .Values.service.type }} Service on port {{ .Values.service.port }}. + +In-cluster endpoint: + + http://{{ include "unstructured-api.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local{{ if ne (.Values.service.port | int) 80 }}:{{ .Values.service.port }}{{ end }} + +Partition endpoint: + + http://{{ include "unstructured-api.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local{{ if ne (.Values.service.port | int) 80 }}:{{ .Values.service.port }}{{ end }}/general/v0/general +{{- else }} +No Service was created (service.enabled=false). Expose the pods yourself to reach the API. +{{- end }} + +{{- if .Values.ingress.enabled }} + +Reachable via Ingress at: +{{- range .Values.ingress.hosts }} +{{- $host := .host }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if .Values.service.enabled }} +{{- if contains "NodePort" .Values.service.type }} + +Get the application URL by running: + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "unstructured-api.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "http://$NODE_IP:$NODE_PORT" +{{- else if contains "LoadBalancer" .Values.service.type }} + +Get the application URL by running (the LoadBalancer may take a moment to provision): + kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "unstructured-api.fullname" . }} +{{- else }} + +Port-forward to reach the API locally: + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "unstructured-api.fullname" . }} 8000:{{ .Values.service.port }} + +Then verify it is ready: + curl --fail http://localhost:8000/healthcheck + +Partition a document: + curl --request POST \ + --url http://localhost:8000/general/v0/general \ +{{- if .Values.apiKey.enabled }} + --header 'unstructured-api-key: ' \ +{{- end }} + --header 'accept: application/json' \ + --form 'files=@sample-docs/family-day.eml' +{{- end }} +{{- end }} + +{{- if .Values.apiKey.enabled }} + +🔐 API-key authentication is enabled. Clients must send the key in the `unstructured-api-key` header. +{{- end }} + +Interactive docs: /general/docs diff --git a/charts/unstructured-api/templates/_helpers.tpl b/charts/unstructured-api/templates/_helpers.tpl new file mode 100644 index 00000000..e7a4d813 --- /dev/null +++ b/charts/unstructured-api/templates/_helpers.tpl @@ -0,0 +1,124 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "unstructured-api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "unstructured-api.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "unstructured-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "unstructured-api.labels" -}} +helm.sh/chart: {{ include "unstructured-api.chart" . }} +{{ include "unstructured-api.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "unstructured-api.selectorLabels" -}} +app.kubernetes.io/name: {{ include "unstructured-api.name" . | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end }} + +{{/* +Create the name of the service account to use. +*/}} +{{- define "unstructured-api.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "unstructured-api.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Name of the Secret holding the API key. +*/}} +{{- define "unstructured-api.apiKeySecretName" -}} +{{- if .Values.apiKey.existingSecret }} +{{- .Values.apiKey.existingSecret }} +{{- else }} +{{- printf "%s-api-key" (include "unstructured-api.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Affinity: explicit .Values.affinity wins, otherwise render the podAntiAffinity preset. +*/}} +{{- define "unstructured-api.affinity" -}} +{{- if .Values.affinity -}} +{{- toYaml .Values.affinity -}} +{{- else if eq .Values.podAntiAffinity "hard" -}} +podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + {{- include "unstructured-api.selectorLabels" . | nindent 10 }} + topologyKey: kubernetes.io/hostname +{{- else if eq .Values.podAntiAffinity "soft" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "unstructured-api.selectorLabels" . | nindent 12 }} + topologyKey: kubernetes.io/hostname +{{- end -}} +{{- end -}} + +{{/* +Validate configuration. +*/}} +{{- define "unstructured-api.validateValues" -}} +{{- if and .Values.apiKey.enabled (not .Values.apiKey.existingSecret) (not .Values.apiKey.value) -}} +{{- fail "apiKey.enabled is true but neither apiKey.value nor apiKey.existingSecret is set." -}} +{{- end -}} +{{- if and .Values.apiKey.enabled .Values.apiKey.existingSecret (not .Values.apiKey.existingSecretKey) -}} +{{- fail "apiKey.existingSecret is set but apiKey.existingSecretKey is empty." -}} +{{- end -}} +{{- if and .Values.autoscaling.enabled (not .Values.autoscaling.targetCPUUtilizationPercentage) (not .Values.autoscaling.targetMemoryUtilizationPercentage) -}} +{{- fail "autoscaling.enabled requires targetCPUUtilizationPercentage or targetMemoryUtilizationPercentage to be set." -}} +{{- end -}} +{{- if and .Values.podDisruptionBudget.enabled (eq (toString .Values.podDisruptionBudget.minAvailable) "") (eq (toString .Values.podDisruptionBudget.maxUnavailable) "") -}} +{{- fail "podDisruptionBudget.enabled requires minAvailable or maxUnavailable to be set." -}} +{{- end -}} +{{- if and .Values.config.parallelMode.enabled (not .Values.config.parallelMode.url) -}} +{{- fail "config.parallelMode.enabled is true but config.parallelMode.url is empty." -}} +{{- end -}} +{{- if and .Values.ingress.enabled (not .Values.service.enabled) -}} +{{- fail "ingress.enabled requires service.enabled=true." -}} +{{- end -}} +{{- if not (has .Values.podAntiAffinity (list "" "soft" "hard")) -}} +{{- fail "podAntiAffinity must be one of: \"\", soft, hard." -}} +{{- end -}} +{{- end -}} diff --git a/charts/unstructured-api/templates/deployment.yaml b/charts/unstructured-api/templates/deployment.yaml new file mode 100644 index 00000000..9cb7a446 --- /dev/null +++ b/charts/unstructured-api/templates/deployment.yaml @@ -0,0 +1,160 @@ +{{- include "unstructured-api.validateValues" . -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "unstructured-api.fullname" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + {{- with .Values.progressDeadlineSeconds }} + progressDeadlineSeconds: {{ . }} + {{- end }} + selector: + matchLabels: + {{- include "unstructured-api.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- if and .Values.apiKey.enabled (not .Values.apiKey.existingSecret) }} + checksum/api-key: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "unstructured-api.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "unstructured-api.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automount }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.config.port }} + protocol: TCP + env: + - name: PORT + value: {{ .Values.config.port | quote }} + - name: HOST + value: {{ .Values.config.host | quote }} + - name: WORKERS + value: {{ .Values.config.workers | quote }} + - name: UNSTRUCTURED_MEMORY_FREE_MINIMUM_MB + value: {{ .Values.config.memoryFreeMinimumMb | quote }} + {{- if .Values.config.env }} + - name: ENV + value: {{ .Values.config.env | quote }} + {{- end }} + {{- if .Values.config.allowedOrigins }} + - name: ALLOWED_ORIGINS + value: {{ .Values.config.allowedOrigins | quote }} + {{- end }} + {{- if .Values.config.maxLifetimeSeconds }} + - name: MAX_LIFETIME_SECONDS + value: {{ .Values.config.maxLifetimeSeconds | quote }} + {{- end }} + {{- if .Values.config.parallelMode.enabled }} + - name: UNSTRUCTURED_PARALLEL_MODE_ENABLED + value: "true" + - name: UNSTRUCTURED_PARALLEL_MODE_URL + value: {{ .Values.config.parallelMode.url | quote }} + - name: UNSTRUCTURED_PARALLEL_MODE_THREADS + value: {{ .Values.config.parallelMode.threads | quote }} + - name: UNSTRUCTURED_PARALLEL_MODE_SPLIT_SIZE + value: {{ .Values.config.parallelMode.splitSize | quote }} + - name: UNSTRUCTURED_PARALLEL_RETRY_ATTEMPTS + value: {{ .Values.config.parallelMode.retryAttempts | quote }} + {{- end }} + {{- if .Values.apiKey.enabled }} + - name: UNSTRUCTURED_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "unstructured-api.apiKeySecretName" . }} + key: {{ if .Values.apiKey.existingSecret }}{{ .Values.apiKey.existingSecretKey }}{{ else }}UNSTRUCTURED_API_KEY{{ end }} + {{- end }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.startupProbe.enabled }} + startupProbe: + httpGet: + path: /healthcheck + port: http + initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.startupProbe.periodSeconds }} + timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }} + failureThreshold: {{ .Values.startupProbe.failureThreshold }} + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /healthcheck + port: http + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: /healthcheck + port: http + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (include "unstructured-api.affinity" .) }} + affinity: + {{- . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/unstructured-api/templates/hpa.yaml b/charts/unstructured-api/templates/hpa.yaml new file mode 100644 index 00000000..4f70d1ae --- /dev/null +++ b/charts/unstructured-api/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled -}} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "unstructured-api.fullname" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "unstructured-api.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/unstructured-api/templates/ingress.yaml b/charts/unstructured-api/templates/ingress.yaml new file mode 100644 index 00000000..e202ff42 --- /dev/null +++ b/charts/unstructured-api/templates/ingress.yaml @@ -0,0 +1,37 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "unstructured-api.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/unstructured-api/templates/pdb.yaml b/charts/unstructured-api/templates/pdb.yaml new file mode 100644 index 00000000..e6e9a84c --- /dev/null +++ b/charts/unstructured-api/templates/pdb.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podDisruptionBudget.enabled -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "unstructured-api.fullname" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} +spec: + {{- if ne (toString .Values.podDisruptionBudget.minAvailable) "" }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else if ne (toString .Values.podDisruptionBudget.maxUnavailable) "" }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "unstructured-api.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/unstructured-api/templates/secret.yaml b/charts/unstructured-api/templates/secret.yaml new file mode 100644 index 00000000..f04148b8 --- /dev/null +++ b/charts/unstructured-api/templates/secret.yaml @@ -0,0 +1,11 @@ +{{- if and .Values.apiKey.enabled (not .Values.apiKey.existingSecret) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "unstructured-api.apiKeySecretName" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} +type: Opaque +stringData: + UNSTRUCTURED_API_KEY: {{ .Values.apiKey.value | quote }} +{{- end }} diff --git a/charts/unstructured-api/templates/service.yaml b/charts/unstructured-api/templates/service.yaml new file mode 100644 index 00000000..66e8e1ee --- /dev/null +++ b/charts/unstructured-api/templates/service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.service.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "unstructured-api.fullname" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "unstructured-api.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/unstructured-api/templates/serviceaccount.yaml b/charts/unstructured-api/templates/serviceaccount.yaml new file mode 100644 index 00000000..6030d0b6 --- /dev/null +++ b/charts/unstructured-api/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "unstructured-api.serviceAccountName" . }} + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/unstructured-api/templates/tests/test-connection.yaml b/charts/unstructured-api/templates/tests/test-connection.yaml new file mode 100644 index 00000000..3f6dc8e7 --- /dev/null +++ b/charts/unstructured-api/templates/tests/test-connection.yaml @@ -0,0 +1,35 @@ +{{- if .Values.service.enabled -}} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "unstructured-api.fullname" . }}-test-healthcheck" + labels: + {{- include "unstructured-api.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 100 + runAsGroup: 100 + seccompProfile: + type: RuntimeDefault + containers: + - name: healthcheck + image: curlimages/curl:8.11.0 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + command: + - curl + args: + - --fail + - --silent + - --show-error + - "http://{{ include "unstructured-api.fullname" . }}:{{ .Values.service.port }}/healthcheck" +{{- end }} diff --git a/charts/unstructured-api/values.yaml b/charts/unstructured-api/values.yaml new file mode 100644 index 00000000..776263e7 --- /dev/null +++ b/charts/unstructured-api/values.yaml @@ -0,0 +1,196 @@ +# -- Number of replicas. Ignored when autoscaling.enabled is true. +replicaCount: 1 + +image: + # -- Container image repository. + repository: quay.io/unstructured-io/unstructured-api + # -- Image pull policy. + pullPolicy: IfNotPresent + # -- Image tag. Defaults to the chart appVersion when left empty. + tag: "" + +# -- Image pull secrets for private registries. +imagePullSecrets: [] +# -- Override the chart name used in resource names. +nameOverride: "" +# -- Override the fully-qualified app name used in resource names. +fullnameOverride: "" + +serviceAccount: + # -- Create a ServiceAccount for the workload. + create: true + # -- Annotations to add to the ServiceAccount (e.g. IRSA / Workload Identity). + annotations: {} + # -- Name of the ServiceAccount. Generated from the fullname when empty. + name: "" + # -- Automount the ServiceAccount API token. + automount: false + +# -- Extra annotations for the Deployment's pod template. +podAnnotations: {} +# -- Extra labels for the Deployment's pod template. +podLabels: {} + +# Pod-level security context. Defaults match the image's non-root user (uid/gid 1000). +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# Container-level security context. +# NOTE: readOnlyRootFilesystem is intentionally false. Partitioning shells out to +# LibreOffice/Tesseract, which write to $HOME and temp paths at runtime. +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL + +service: + # -- Create a Service. Ingress and the helm test require it. + enabled: true + # -- Kubernetes Service type. + type: ClusterIP + # -- Service port. + port: 80 + # -- Extra Service annotations. + annotations: {} + +# Container / application configuration, surfaced as environment variables. +config: + # -- Port the API listens on inside the container (PORT). + port: 8000 + # -- Interface the API binds to (HOST). + host: "0.0.0.0" + # -- Uvicorn workers (WORKERS). Each one loads its own copy of the models; prefer more + # replicas over more workers. + workers: 1 + # -- Reject new work with HTTP 503 below this amount of free memory in MB + # (UNSTRUCTURED_MEMORY_FREE_MINIMUM_MB). Set to 0 to disable. + memoryFreeMinimumMb: 2048 + # -- Comma-separated CORS origins (ALLOWED_ORIGINS). Empty disables CORS. + allowedOrigins: "" + # -- Begin graceful shutdown after this many seconds (MAX_LIFETIME_SECONDS). Empty disables. + maxLifetimeSeconds: "" + # -- Sets ENV, which silences the noisy uvicorn error logger. Typically "prod". + env: "prod" + + parallelMode: + # -- Enable experimental parallel PDF mode (UNSTRUCTURED_PARALLEL_MODE_ENABLED). + enabled: false + # -- Partition Endpoint URL that receives page ranges (UNSTRUCTURED_PARALLEL_MODE_URL). + url: "" + # -- Max concurrent remote requests (UNSTRUCTURED_PARALLEL_MODE_THREADS). + threads: 3 + # -- Pages per remote request (UNSTRUCTURED_PARALLEL_MODE_SPLIT_SIZE). + splitSize: 1 + # -- Retries after the initial request (UNSTRUCTURED_PARALLEL_RETRY_ATTEMPTS). + retryAttempts: 2 + +# API-key authentication (UNSTRUCTURED_API_KEY). When set, clients must send the same +# value in the `unstructured-api-key` request header. +apiKey: + # -- Enable API-key authentication. + enabled: false + # -- API-key value. Rendered into a Secret managed by this chart. Leave empty and use + # existingSecret to manage the key out-of-band. + value: "" + # -- Reference an existing Secret instead of creating one. + existingSecret: "" + # -- Key within existingSecret that holds the API key. + existingSecretKey: "UNSTRUCTURED_API_KEY" + +# -- Extra environment variables (list of name/value or name/valueFrom maps). +extraEnv: [] +# -- Extra environment variables sourced from ConfigMaps/Secrets via envFrom. +extraEnvFrom: [] + +# -- Resource requests/limits. +resources: + requests: + cpu: 500m + memory: 3Gi + limits: + cpu: "4" + memory: 8Gi + +# Probes hit /healthcheck. It only confirms the HTTP server is up, not that models are +# loaded or that memory is free, so readiness won't drain an overloaded pod. +livenessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 +readinessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +# Covers process start only (a few seconds), not model loading. +startupProbe: + enabled: true + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 6 + +autoscaling: + # -- Enable a HorizontalPodAutoscaler. + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + # -- Target memory utilization percentage (optional). + targetMemoryUtilizationPercentage: 0 + +# PodDisruptionBudget. Set exactly one of maxUnavailable / minAvailable (minAvailable wins +# if both are set). maxUnavailable is safer at low replica counts: minAvailable: 1 with a +# single replica blocks every voluntary disruption and stalls node drains. +podDisruptionBudget: + enabled: false + maxUnavailable: 1 + minAvailable: "" + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: unstructured-api.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: unstructured-api-tls + # hosts: + # - unstructured-api.local + +# -- Grace period for in-flight requests on shutdown. hi_res requests can run for a +# couple of minutes, so this is well above the 30s default to avoid killing them. +terminationGracePeriodSeconds: 120 + +# -- Rollout deadline. +progressDeadlineSeconds: 900 + +# -- Node selector for pod scheduling. +nodeSelector: {} +# -- Tolerations for pod scheduling. +tolerations: [] +# -- Spread replicas across nodes: "soft" (preferred), "hard" (one per node), or "" to +# disable. Ignored when `affinity` is set. +podAntiAffinity: soft +# -- Affinity rules for pod scheduling. Overrides podAntiAffinity when set. +affinity: {} +# -- Topology spread constraints. +topologySpreadConstraints: [] + +# -- Additional volumes for the pod. +volumes: [] +# -- Additional volume mounts for the container. +volumeMounts: []