diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 0000000..e47937e --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,52 @@ +name: Build / Deploy Helm Chart + +on: + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4.3.0 + with: + version: 'latest' + + - name: Update Helm chart dependencies + run: | + cd ./helm/opengist + helm dependency update + + - name: Package Helm chart + run: | + cd ./helm + helm package ./opengist + + # First time, create the index + if [ ! -f index.yaml ]; then + helm repo index --url https://helm.opengist.io . + else + # For subsequent runs, merge with existing index + helm repo index --url https://helm.opengist.io --merge index.yaml . + fi + + - name: Deploy to server + uses: appleboy/scp-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_SSH_KEY }} + source: "./helm/*.tgz,./helm/index.yaml" + target: ${{ secrets.HELM_SERVER_PATH }} + + - name: Update remote helm repository + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_SSH_KEY }} + script: | + ${{ secrets.UPDATE_HELM_REPO }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 762d863..ca7f8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ gist.db /**/.DS_Store public/assets/* public/manifest.json +./opengist opengist build/ docs/.vitepress/dist/ docs/.vitepress/cache/ +helm/opengist/charts/ \ No newline at end of file diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index a698682..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# kustomize - -## Simple - -`kustomization.yaml`: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -metadata: - name: opengist - -resources: - - https://github.com/thomiceli/opengist/deploy/ -``` - -## Full example - -`kustomization.yaml`: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -metadata: - name: opengist - -namespace: opengist - -resources: - - namespace.yaml - - https://github.com/thomiceli/opengist/deploy/?ref:v1.9.1 - -images: - - name: ghcr.io/thomiceli/opengist - newTag: 1.9.1 - -patches: - # Add your ingress - - path: ingress.yaml - - patch: |- - - op: add - path: /spec/rules/0/host - value: opengist.mydomain.com - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: opengist -``` - -`namespace.yaml`: - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: opengist -``` - -`ingress.yaml`: - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: opengist - annotations: - cert-manager.io/cluster-issuer: letsencrypt-production -spec: - ingressClassName: nginx - tls: - - hosts: - - opengist.mydomain.com - secretName: opengist-tls -``` diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml deleted file mode 100644 index d67817f..0000000 --- a/deploy/deployment.yaml +++ /dev/null @@ -1,29 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: opengist -spec: - selector: - matchLabels: - app.kubernetes.io/name: opengist - template: - metadata: - labels: - app.kubernetes.io/name: opengist - spec: - containers: - - name: opengist - image: ghcr.io/thomiceli/opengist - ports: - - name: http - containerPort: 6157 - - name: ssh - containerPort: 2222 - volumeMounts: - - mountPath: /opengist - name: data - volumes: - - name: data - persistentVolumeClaim: - claimName: opengist-data diff --git a/deploy/ingress.yaml b/deploy/ingress.yaml deleted file mode 100644 index be2ad05..0000000 --- a/deploy/ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: opengist - labels: - app.kubernetes.io/name: opengist - app.kubernetes.io/component: ingress -spec: - rules: - - host: opengist.local - http: - paths: - - pathType: Prefix - path: "/" - backend: - service: - name: opengist - port: - name: http diff --git a/deploy/kustomization.yaml b/deploy/kustomization.yaml deleted file mode 100644 index 556d8b7..0000000 --- a/deploy/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -metadata: - name: opengist - -resources: - - deployment.yaml - - pvc.yaml - - ingress.yaml - - service.yaml diff --git a/deploy/pvc.yaml b/deploy/pvc.yaml deleted file mode 100644 index 52afe47..0000000 --- a/deploy/pvc.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: opengist-data - labels: - app.kubernetes.io/name: opengist - app.kubernetes.io/component: data -spec: - resources: - requests: - storage: 1Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce diff --git a/deploy/service.yaml b/deploy/service.yaml deleted file mode 100644 index 921a857..0000000 --- a/deploy/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: opengist - labels: - app.kubernetes.io/name: opengist -spec: - selector: - app.kubernetes.io/name: opengist - ports: - - port: 80 - targetPort: http - name: http diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index bd7625b..99fc465 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -25,6 +25,7 @@ export default defineConfig({ {text: 'Introduction', link: '/docs'}, {text: 'Installation', link: '/docs/installation', items: [ {text: 'Docker', link: '/docs/installation/docker'}, + {text: 'Kubernetes', link: '/docs/installation/kubernetes'}, {text: 'Binary', link: '/docs/installation/binary'}, {text: 'Source', link: '/docs/installation/source'}, ], diff --git a/docs/installation/kubernetes.md b/docs/installation/kubernetes.md new file mode 100644 index 0000000..dedca4e --- /dev/null +++ b/docs/installation/kubernetes.md @@ -0,0 +1,15 @@ +# Install on Kubernetes + +A [Helm](https://helm.sh) chart is available to install Opengist on a Kubernetes cluster. +Check the [Helm documentation](https://helm.sh/docs/) for more information on how to use Helm. + +A non-customized installation of Opengist can be done with: + +```bash +helm repo add opengist https://helm.opengist.io + +helm install opengist opengist/opengist +``` + +Refer to the [Opengist chart](https://github.com/thomiceli/opengist/tree/master/helm/opengist) for more information +about the chart and to customize your installation. \ No newline at end of file diff --git a/helm/opengist/.helmignore b/helm/opengist/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/opengist/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/opengist/Chart.lock b/helm/opengist/Chart.lock new file mode 100644 index 0000000..7eef97a --- /dev/null +++ b/helm/opengist/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: postgresql + repository: oci://registry-1.docker.io/bitnamicharts + version: 16.5.6 +- name: meilisearch + repository: https://meilisearch.github.io/meilisearch-kubernetes + version: 0.12.0 +digest: sha256:31084e570aa16e3a26317aeb6d0d5dec62540c314ee4f703374e6e7827399fa6 +generated: "2025-03-27T11:34:51.840778733+01:00" diff --git a/helm/opengist/Chart.yaml b/helm/opengist/Chart.yaml new file mode 100644 index 0000000..5b71124 --- /dev/null +++ b/helm/opengist/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: opengist +description: Opengist Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: 1.10.0 +home: https://opengist.io +icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg +sources: + - https://github.com/thomiceli/opengist +dependencies: + - name: postgresql + repository: oci://registry-1.docker.io/bitnamicharts + version: 16.5.6 + condition: postgresql.enabled + - name: meilisearch + repository: https://meilisearch.github.io/meilisearch-kubernetes + version: 0.12.0 + condition: meilisearch.enabled diff --git a/helm/opengist/README.md b/helm/opengist/README.md new file mode 100644 index 0000000..b5fba2f --- /dev/null +++ b/helm/opengist/README.md @@ -0,0 +1,81 @@ +# Opengist Helm Chart + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![AppVersion: 1.10.0](https://img.shields.io/badge/AppVersion-1.10.0-informational?style=flat-square) + +Opengist Helm chart for Kubernetes. + +* [Install](#install) +* [Configuration](#configuration) +* [Dependencies](#dependencies) + * [Meilisearch Indexer](#meilisearch-indexer) + * [PostgreSQL Database](#postgresql-database) + +## Install + +```bash +helm repo add opengist https://helm.opengist.io + +helm install opengist opengist/opengist +``` + +## Configuration + +This part explains how to configure the Opengist instance using the Helm chart. The `config.yml` file used by Opengist +is mounted from a Kubernetes Secret with a key `config.yml` and the values formatted as YAML. + +### Using values + +Using Helm values, you can define the values from a key name `config` + +```yaml +config: + log-level: "warn" + log-output: "stdout" +``` + +This will create a Kubernetes secret named `opengist` mounted to the pod as a file with the YAML content of the secret, +used by Opengist. + +### Using an existing secret + +If you wish to not store sensitive data in your Helm values, you can create a Kubernetes secret with a key `config.yml` +and values formatted as YAML. You can then reference this secret in the Helm chart with the `configExistingSecret` key. + +If defined, this existing secret will be used instead of creating a new one. + +```yaml +configExistingSecret: +``` + +## Dependencies + +### Meilisearch Indexer + +By default, Opengist uses the `bleve` indexer. **It is NOT available** if there is multiple replicas of the opengist pod (only one pod can open the index at the same time). + +Instead, for multiple replicas setups, you **MUST** use the `meilisearch` indexer. + +By setting `meilisearch.enabled: true`, the [Meilisearch chart](https://github.com/meilisearch/meilisearch-kubernetes) will be deployed aswell. +You must define the `meilisearch.host` (Kubernetes Service) and `meilisearch.key` (value created by Meilisearch) values to connect to the Meilisearch instance in your Opengist config : + +```yaml +index: meilisearch +index.meili.host: http://opengist-meilisearch:7700 # pointing to the K8S Service +index.meili.api-key: MASTER_KEY # generated by Meilisearch +``` + +If you want to use the `bleve` indexer, you need to set the `replicas` to `1`. + +### PostgreSQL Database + +By default, Opengist uses the `sqlite` database. If needed, this chart also deploys a PostgreSQL instance. + +By setting `postgresql.enabled: true`, the [Bitnami PostgreSQL chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) will be deployed aswell. +You must define the `postgresql.host`, `postgresql.port`, `postgresql.database`, `postgresql.username` and `postgresql.password` values to connect to the PostgreSQL instance. + +Then define the connection string in your Opengist config: + +```yaml +db-uri: postgres://user:password@opengist-postgresql:5432/opengist +``` +Note: `opengist-postgresql` is the name of the K8S Service deployed by this chart. diff --git a/helm/opengist/templates/NOTES.txt b/helm/opengist/templates/NOTES.txt new file mode 100644 index 0000000..0358c3f --- /dev/null +++ b/helm/opengist/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.http.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "opengist.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.http.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "opengist.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "opengist.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.http.port }} +{{- else if contains "ClusterIP" .Values.service.http.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "opengist.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/opengist/templates/_helpers.tpl b/helm/opengist/templates/_helpers.tpl new file mode 100644 index 0000000..7b20fe8 --- /dev/null +++ b/helm/opengist/templates/_helpers.tpl @@ -0,0 +1,85 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "opengist.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). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "opengist.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 "opengist.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "opengist.labels" -}} +helm.sh/chart: {{ include "opengist.chart" . }} +app: {{ include "opengist.name" . }} +{{ include "opengist.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "opengist.selectorLabels" -}} +app.kubernetes.io/name: {{ include "opengist.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "opengist.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "opengist.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create image URI +*/}} +{{- define "opengist.image" -}} +{{- if .Values.image.digest -}} +{{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} +{{- else -}} +{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) -}} +{{- end -}} +{{- end -}} + +{{/* +Create secret name +*/}} +{{- define "opengist.secretName" -}} +{{- if .Values.configExistingSecret -}} +{{- printf "%s" (tpl .Values.configExistingSecret $) -}} +{{- else -}} +{{- printf "%s" (include "opengist.fullname" .) -}} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm/opengist/templates/deployment.yaml b/helm/opengist/templates/deployment.yaml new file mode 100644 index 0000000..02a706b --- /dev/null +++ b/helm/opengist/templates/deployment.yaml @@ -0,0 +1,115 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "opengist.fullname" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- if .Values.deployment.labels }} + {{- toYaml .Values.deployment.labels | nindent 4 }} + {{- end }} + {{- with .Values.deployment.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "opengist.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "opengist.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "opengist.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: init-config + image: busybox:1.37 + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'cp /init/config/config.yml /config-volume/config.yml'] + volumeMounts: + - name: config-secret + mountPath: /init/config + - name: config-volume + mountPath: /config-volume + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.http.port }} + protocol: TCP + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + {{- toYaml (omit .Values.livenessProbe "enabled") | nindent 12 }} + httpGet: + port: http + path: /healthcheck + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + {{- toYaml (omit .Values.readinessProbe "enabled") | nindent 12 }} + httpGet: + port: http + path: /healthcheck + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: config-volume + mountPath: /config.yml + subPath: config.yml + - name: opengist-data + mountPath: /opengist + {{- if gt (len .Values.extraVolumeMounts) 0 }} + {{- toYaml .Values.extraVolumeMounts | nindent 12 }} + {{- end }} + volumes: + - name: opengist-data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "opengist.fullname" . }}-data + {{- else }} + emptyDir: {} + {{- end }} + - name: config-secret + secret: + secretName: {{ include "opengist.secretName" . }} + defaultMode: 511 + - name: config-volume + emptyDir: {} + {{- if gt (len .Values.extraVolumes) 0 }} + {{- toYaml .Values.extraVolumes | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/opengist/templates/hpa.yaml b/helm/opengist/templates/hpa.yaml new file mode 100644 index 0000000..995e8b3 --- /dev/null +++ b/helm/opengist/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "opengist.fullname" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- with .Values.autoscaling.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "opengist.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/helm/opengist/templates/ingress.yaml b/helm/opengist/templates/ingress.yaml new file mode 100644 index 0000000..1192489 --- /dev/null +++ b/helm/opengist/templates/ingress.yaml @@ -0,0 +1,47 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "opengist.fullname" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- if .Values.ingress.labels }} + {{- toYaml .Values.service.http.labels | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "opengist.fullname" $ }}-http + port: + number: {{ $.Values.service.http.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/opengist/templates/pdb.yaml b/helm/opengist/templates/pdb.yaml new file mode 100644 index 0000000..a222c7e --- /dev/null +++ b/helm/opengist/templates/pdb.yaml @@ -0,0 +1,14 @@ +{{- if .Values.podDisruptionBudget -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "opengist.fullname" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "opengist.selectorLabels" . | nindent 6 }} + {{- toYaml .Values.podDisruptionBudget | nindent 2 }} +{{- end -}} \ No newline at end of file diff --git a/helm/opengist/templates/pvc.yaml b/helm/opengist/templates/pvc.yaml new file mode 100644 index 0000000..f4eaa2b --- /dev/null +++ b/helm/opengist/templates/pvc.yaml @@ -0,0 +1,28 @@ +{{- if .Values.persistence.enabled }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "opengist.fullname" . }}-data + namespace: {{ .Release.Namespace }} +{{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- if .Values.persistence.labels }} + {{- toYaml .Values.persistence.labels | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- if gt .Values.replicaCount 1.0 }} + - ReadWriteMany + {{- else }} + {{- .Values.persistence.accessModes | toYaml | nindent 4 }} + {{- end }} + volumeMode: Filesystem + storageClassName: {{ .Values.persistence.storageClass | quote }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} \ No newline at end of file diff --git a/helm/opengist/templates/secret.yaml b/helm/opengist/templates/secret.yaml new file mode 100644 index 0000000..ac1fe3f --- /dev/null +++ b/helm/opengist/templates/secret.yaml @@ -0,0 +1,13 @@ +{{- if (not .Values.configExistingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "opengist.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "opengist.labels" . | indent 4 }} +type: Opaque +stringData: + config.yml: |- + {{- .Values.config | toYaml | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/opengist/templates/serviceaccount.yaml b/helm/opengist/templates/serviceaccount.yaml new file mode 100644 index 0000000..d04e8b9 --- /dev/null +++ b/helm/opengist/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "opengist.serviceAccountName" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/opengist/templates/svc-http.yaml b/helm/opengist/templates/svc-http.yaml new file mode 100644 index 0000000..52762e2 --- /dev/null +++ b/helm/opengist/templates/svc-http.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "opengist.fullname" . }}-http + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- if .Values.service.http.labels }} + {{- toYaml .Values.service.http.labels | nindent 4 }} + {{- end }} + {{- with .Values.service.http.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + +spec: + type: {{ .Values.service.http.type }} + {{- if eq .Values.service.http.type "LoadBalancer" }} + {{- if and .Values.service.http.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.http.loadBalancerIP }} + {{- end }} + {{- if .Values.service.http.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- range .Values.service.http.loadBalancerSourceRanges }} + - {{ . }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.service.http.externalIPs }} + externalIPs: + {{- toYaml .Values.service.http.externalIPs | nindent 4 }} + {{- end }} + {{- if .Values.service.http.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.http.externalTrafficPolicy }} + {{- end }} + {{- if and .Values.service.http.clusterIP (eq .Values.service.http.type "ClusterIP") }} + clusterIP: {{ .Values.service.http.clusterIP }} + {{- end }} + ports: + - name: http + port: {{ .Values.service.http.port }} + {{- if .Values.service.http.nodePort }} + nodePort: {{ .Values.service.http.nodePort }} + {{- end }} + targetPort: {{ index .Values.config "http.port" }} + selector: + {{- include "opengist.selectorLabels" . | nindent 4 }} diff --git a/helm/opengist/templates/svc-ssh.yaml b/helm/opengist/templates/svc-ssh.yaml new file mode 100644 index 0000000..8f782b1 --- /dev/null +++ b/helm/opengist/templates/svc-ssh.yaml @@ -0,0 +1,64 @@ +{{- if .Values.service.ssh.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "opengist.fullname" . }}-ssh + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "opengist.labels" . | nindent 4 }} + {{- if .Values.service.ssh.labels }} + {{- toYaml .Values.service.ssh.labels | nindent 4 }} + {{- end }} + {{- with .Values.service.http.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.ssh.type }} + {{- if eq .Values.service.ssh.type "LoadBalancer" }} + {{- if .Values.service.ssh.loadBalancerClass }} + loadBalancerClass: {{ .Values.service.ssh.loadBalancerClass }} + {{- end }} + {{- if .Values.service.ssh.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.ssh.loadBalancerIP }} + {{- end -}} + {{- if .Values.service.ssh.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- range .Values.service.ssh.loadBalancerSourceRanges }} + - {{ . }} + {{- end }} + {{- end }} + {{- end }} + {{- if and .Values.service.ssh.clusterIP (eq .Values.service.ssh.type "ClusterIP") }} + clusterIP: {{ .Values.service.ssh.clusterIP }} + {{- end }} + {{- if .Values.service.ssh.externalIPs }} + externalIPs: + {{- toYaml .Values.service.ssh.externalIPs | nindent 4 }} + {{- end }} + {{- if .Values.service.ssh.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.service.ssh.ipFamilyPolicy }} + {{- end }} + {{- with .Values.service.ssh.ipFamilies }} + ipFamilies: + {{- toYaml . | nindent 4 }} + {{- end -}} + {{- if .Values.service.ssh.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.ssh.externalTrafficPolicy }} + {{- end }} + ports: + - name: ssh + port: {{ .Values.service.ssh.port }} + {{- if .Values.service.ssh.nodePort }} + nodePort: {{ .Values.service.ssh.nodePort }} + {{- end }} + {{- if index .Values.config "ssh.port" }} + targetPort: {{ index .Values.config "ssh.port" }} + {{- else }} + targetPort: 2222 + {{- end }} + protocol: TCP + + selector: + {{- include "opengist.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/opengist/templates/tests/test-connection.yaml b/helm/opengist/templates/tests/test-connection.yaml new file mode 100644 index 0000000..dc7ef30 --- /dev/null +++ b/helm/opengist/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "opengist.fullname" . }}-test-connection" + labels: + {{- include "opengist.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "opengist.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/opengist/values.yaml b/helm/opengist/values.yaml new file mode 100644 index 0000000..1c5f125 --- /dev/null +++ b/helm/opengist/values.yaml @@ -0,0 +1,201 @@ +## Kubernetes workload configuration for Opengist +nameOverride: "" +fullnameOverride: "" +namespace: "" + +## Opengist YAML Application Config. See more at https://opengist.io/docs/configuration/cheat-sheet.html +## This will create a Kubernetes secret with the key `config.yml` containing the YAML configuration mounted in the pod. +config: + log-level: "warn" + log-output: "stdout" + +## If defined, the existing secret will be used instead of creating a new one. +## The secret must contain a key named `config.yml` with the YAML configuration. +configExistingSecret: "" + +## Define the image repository and tag to use. +image: + repository: ghcr.io/thomiceli/opengist + pullPolicy: Always + tag: "1.10.0" + digest: "" +imagePullSecrets: [] +# - name: "image-pull-secret" + +## Define the deployment replica count +replicaCount: 1 + +## Define the deployment strategy type +strategy: + type: "RollingUpdate" + rollingUpdate: + maxSurge: "100%" + maxUnavailable: 0 + +## Security Context settings +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +podSecurityContext: + fsGroup: 1000 +securityContext: {} +# allowPrivilegeEscalation: false + +## Pod Disruption Budget settings +## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ +podDisruptionBudget: {} +# maxUnavailable: 1 +# minAvailable: 1 + +## Set the Kubernetes service type +## ref: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + http: + type: ClusterIP + clusterIP: + port: 6157 + nodePort: + loadBalancerIP: + externalIPs: [] + labels: {} + annotations: {} + loadBalancerSourceRanges: [] + externalTrafficPolicy: + + ssh: + enabled: true + type: ClusterIP + clusterIP: + port: 2222 + nodePort: + loadBalancerIP: + externalIPs: [] + labels: {} + annotations: {} + loadBalancerSourceRanges: [] + externalTrafficPolicy: + +## HTTP Ingress for Opengist +## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + labels: {} + # node-role.kubernetes.io/ingress: platform + annotations: {} + # kubernetes.io/ingress.class: nginx + hosts: + - host: opengist.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: opengist-tls + # hosts: + # - opengist.example.com + +## Service Account for Opengist pods +## ref: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + create: true + annotations: {} + name: "" + +## Set persistence using a Persistent Volume Claim +## If more than 2 replicas are set, the access mode must be ReadWriteMany +## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ +persistence: + enabled: true + existingClaim: "" + storageClass: "" + labels: {} + annotations: + helm.sh/resource-policy: keep + size: 5Gi + accessModes: + - ReadWriteOnce + subPath: "" + +extraVolumes: [] +extraVolumeMounts: [] + +## Additional pod labels and annotations +podLabels: {} +podAnnotations: {} + +## Configure resource requests and limits +## ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ +resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + +## Configure the liveness and readiness probes +## ref: https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/ +livenessProbe: + enabled: true + initialDelaySeconds: 200 + timeoutSeconds: 1 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 5 + +readinessProbe: + enabled: true + initialDelaySeconds: 5 + timeoutSeconds: 1 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + +## Define autoscaling configuration using Horizontal Pod Autoscaler +## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + annotations: {} + +## Additional deployment configuration +deployment: + env: [] + terminationGracePeriodSeconds: 60 + labels: {} + annotations: {} + +## Set pod assignment with node labels +## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ +nodeSelector: {} +tolerations: [] +affinity: {} + +## Use PostgreSQL as a database, using Bitnami's PostgreSQL Helm chart +## ref: https://artifacthub.io/packages/helm/bitnami/postgresql/16.5.6 +postgresql: + enabled: false + global: + postgresql: + auth: + username: opengist + password: opengist + database: opengist + service: + ports: + postgresql: 5432 + primary: + persistence: + size: 10Gi + + +## Use Meilisearch as a code indexer, using Meilisearch's Helm chart +## ref: https://github.com/meilisearch/meilisearch-kubernetes/tree/meilisearch-0.12.0 +meilisearch: + enabled: false + environment: + MEILI_ENV: "production" + auth: + existingMasterKeySecret: diff --git a/internal/cli/main.go b/internal/cli/main.go index 5b07b89..9a832b2 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -126,7 +126,7 @@ func Initialize(ctx *cli.Context) { index.DepreactionIndexDirname() if index.IndexEnabled() { - index.NewIndexer(index.IndexType()) + go index.NewIndexer(index.IndexType()) } } diff --git a/internal/index/bleve.go b/internal/index/bleve.go index 8dec430..3856c00 100644 --- a/internal/index/bleve.go +++ b/internal/index/bleve.go @@ -22,16 +22,23 @@ func NewBleveIndexer(path string) *BleveIndexer { return &BleveIndexer{path: path} } -func (i *BleveIndexer) Init() { +func (i *BleveIndexer) Init() error { + errChan := make(chan error, 1) + go func() { bleveIndex, err := i.open() if err != nil { log.Error().Err(err).Msg("Failed to open Bleve index") i.Close() + errChan <- err + return } i.index = bleveIndex log.Info().Msg("Bleve indexer initialized") + errChan <- nil }() + + return <-errChan } func (i *BleveIndexer) open() (bleve.Index, error) { diff --git a/internal/index/indexer.go b/internal/index/indexer.go index ea988e8..445ddfd 100644 --- a/internal/index/indexer.go +++ b/internal/index/indexer.go @@ -11,7 +11,7 @@ import ( var atomicIndexer atomic.Pointer[Indexer] type Indexer interface { - Init() + Init() error Close() Add(gist *Gist) error Remove(gistID uint) error @@ -64,7 +64,9 @@ func NewIndexer(idxType IndexerType) { return } - idx.Init() + if err := idx.Init(); err != nil { + return + } atomicIndexer.Store(&idx) } @@ -73,12 +75,12 @@ func Close() { return } - idx := *atomicIndexer.Load() + idx := atomicIndexer.Load() if idx == nil { return } - idx.Close() + (*idx).Close() atomicIndexer.Store(nil) } @@ -87,12 +89,12 @@ func AddInIndex(gist *Gist) error { return nil } - idx := *atomicIndexer.Load() + idx := atomicIndexer.Load() if idx == nil { return fmt.Errorf("indexer is not initialized") } - return idx.Add(gist) + return (*idx).Add(gist) } func RemoveFromIndex(gistID uint) error { @@ -100,12 +102,12 @@ func RemoveFromIndex(gistID uint) error { return nil } - idx := *atomicIndexer.Load() + idx := atomicIndexer.Load() if idx == nil { return fmt.Errorf("indexer is not initialized") } - return idx.Remove(gistID) + return (*idx).Remove(gistID) } func SearchGists(query string, metadata SearchGistMetadata, userId uint, page int) ([]uint, uint64, map[string]int, error) { @@ -113,12 +115,12 @@ func SearchGists(query string, metadata SearchGistMetadata, userId uint, page in return nil, 0, nil, nil } - idx := *atomicIndexer.Load() + idx := atomicIndexer.Load() if idx == nil { return nil, 0, nil, fmt.Errorf("indexer is not initialized") } - return idx.Search(query, metadata, userId, page) + return (*idx).Search(query, metadata, userId, page) } func DepreactionIndexDirname() { diff --git a/internal/index/meilisearch.go b/internal/index/meilisearch.go index ad29258..66f7798 100644 --- a/internal/index/meilisearch.go +++ b/internal/index/meilisearch.go @@ -25,29 +25,34 @@ func NewMeiliIndexer(host, apikey, indexName string) *MeiliIndexer { } } -func (i *MeiliIndexer) Init() { +func (i *MeiliIndexer) Init() error { + errChan := make(chan error, 1) + go func() { meiliIndex, err := i.open() if err != nil { log.Error().Err(err).Msg("Failed to open Meilisearch index") i.Close() + errChan <- err + return } i.index = meiliIndex log.Info().Msg("Meilisearch indexer initialized") + errChan <- nil }() + + return <-errChan } func (i *MeiliIndexer) open() (meilisearch.IndexManager, error) { - client := meilisearch.New(i.host, meilisearch.WithAPIKey(i.apikey)) - indexResult, err := client.GetIndex(i.indexName) - if err != nil { - return nil, err - } + i.client = meilisearch.New(i.host, meilisearch.WithAPIKey(i.apikey)) + indexResult, err := i.client.GetIndex(i.indexName) - if indexResult != nil { + if indexResult != nil && err == nil { return indexResult.IndexManager, nil } - _, err = client.CreateIndex(&meilisearch.IndexConfig{ + + _, err = i.client.CreateIndex(&meilisearch.IndexConfig{ Uid: i.indexName, PrimaryKey: "GistID", }) @@ -55,14 +60,14 @@ func (i *MeiliIndexer) open() (meilisearch.IndexManager, error) { return nil, err } - _, _ = client.Index(i.indexName).UpdateSettings(&meilisearch.Settings{ + _, _ = i.client.Index(i.indexName).UpdateSettings(&meilisearch.Settings{ FilterableAttributes: []string{"GistID", "UserID", "Visibility", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"}, DisplayedAttributes: []string{"GistID"}, SearchableAttributes: []string{"Content", "Username", "Title", "Filenames", "Extensions", "Languages", "Topics"}, RankingRules: []string{"words"}, }) - return client.Index(i.indexName), nil + return i.client.Index(i.indexName), nil } func (i *MeiliIndexer) Close() {