diff --git a/README.md b/README.md index c14c9bd..56b61b7 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ to configure itself via the Kubernetes EnvVar API. - [bitnami/mongodb](https://artifacthub.io/packages/helm/bitnami/mongodb) - Redis - [bitnami/redis](https://artifacthub.io/packages/helm/bitnami/redis) +- Meilisearch [beta] + - [meilisearch/meilisearch](https://github.com/meilisearch/meilisearch-kubernetes) ## Installation diff --git a/internal/database/dialect.go b/internal/database/dialect.go index 6a5af0b..5c5898c 100644 --- a/internal/database/dialect.go +++ b/internal/database/dialect.go @@ -8,6 +8,7 @@ import ( "github.com/clevyr/kubedb/internal/config" "github.com/clevyr/kubedb/internal/database/mariadb" + "github.com/clevyr/kubedb/internal/database/meilisearch" "github.com/clevyr/kubedb/internal/database/mongodb" "github.com/clevyr/kubedb/internal/database/postgres" "github.com/clevyr/kubedb/internal/database/redis" @@ -20,6 +21,7 @@ func All() []config.Database { mariadb.MariaDB{}, mongodb.MongoDB{}, redis.Redis{}, + meilisearch.Meilisearch{}, } } diff --git a/internal/database/meilisearch/dump.sh b/internal/database/meilisearch/dump.sh new file mode 100644 index 0000000..33a168e --- /dev/null +++ b/internal/database/meilisearch/dump.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +set -euo pipefail + +curl_api() { + curl -sSfX "${2:-GET}" -H "Authorization: Bearer $MEILI_MASTER_KEY" "$API_HOST/$1" +} + +get_dump_uid() { + curl_api "tasks/$task_uid" | grep -Eo '"dumpUid":"[^,}]+' | cut -d: -f2 | cut -d\" -f2 +} + +echo 'Creating dump' >&2 +if command -v meilitool &>/dev/null; then + meilitool export-a-dump + dump="dumps/$(ls -t1 dumps | head -n1)" +else + task_uid="$(curl_api dumps POST | grep -Eo '"taskUid":[^,}]+' | cut -d: -f2)" + echo 'Waiting for dump to complete' >&2 + while [ -z "${dump_uid:-}" ]; do + dump_uid="$(get_dump_uid || true)" + sleep 1 + done + dump="dumps/$dump_uid.dump" +fi + +echo Downloading dump >&2 +cat "$dump" +echo 'Cleaning up' >&2 +rm -f "$dump" diff --git a/internal/database/meilisearch/meilisearch.go b/internal/database/meilisearch/meilisearch.go new file mode 100644 index 0000000..5bc2c9a --- /dev/null +++ b/internal/database/meilisearch/meilisearch.go @@ -0,0 +1,83 @@ +package meilisearch + +import ( + _ "embed" + "strconv" + + "github.com/clevyr/kubedb/internal/command" + "github.com/clevyr/kubedb/internal/config" + "github.com/clevyr/kubedb/internal/database/sqlformat" + "github.com/clevyr/kubedb/internal/kubernetes" +) + +var ( + _ config.DatabaseDump = Meilisearch{} + _ config.DatabaseRestore = Meilisearch{} + _ config.DatabasePort = Meilisearch{} + _ config.DatabasePassword = Meilisearch{} + _ config.DatabaseDisableJob = Meilisearch{} +) + +type Meilisearch struct{} + +func (Meilisearch) Name() string { + return "meilisearch" +} + +func (Meilisearch) PortEnvNames() kubernetes.ConfigLookups { + return kubernetes.ConfigLookups{} +} + +func (Meilisearch) DefaultPort() uint16 { + return 7700 +} + +func (Meilisearch) PodLabels() []kubernetes.LabelQueryable { + return []kubernetes.LabelQueryable{ + kubernetes.LabelQuery{Name: "app.kubernetes.io/name", Value: "meilisearch"}, + } +} + +func (Meilisearch) PasswordEnvNames(c config.Global) kubernetes.ConfigLookups { + return kubernetes.ConfigLookups{ + kubernetes.LookupEnv{"MEILI_MASTER_KEY"}, + kubernetes.LookupNop{}, + } +} + +//go:embed dump.sh +var dumpScript string + +func (Meilisearch) DumpCommand(conf config.Dump) *command.Builder { + url := "http://" + conf.Host + ":" + strconv.Itoa(int(conf.Port)) + cmd := command.NewBuilder( + command.NewEnv("API_HOST", url), + "sh", "-c", dumpScript, + ) + if conf.Password != "" { + cmd.Unshift(command.NewEnv("MEILI_MASTER_KEY", conf.Password)) + } + return cmd +} + +//go:embed restore.sh +var restoreScript string + +func (Meilisearch) RestoreCommand(conf config.Restore, _ sqlformat.Format) *command.Builder { + cmd := command.NewBuilder("sh", "-c", restoreScript) + if conf.Password != "" { + cmd.Unshift(command.NewEnv("MEILI_MASTER_KEY", conf.Password)) + } + return cmd +} + +func (Meilisearch) Formats() map[sqlformat.Format]string { + return map[sqlformat.Format]string{ + sqlformat.Plain: ".dump", + sqlformat.Gzip: ".dump.gz", + } +} + +func (Meilisearch) DisableJob() bool { + return true +} diff --git a/internal/database/meilisearch/restore.sh b/internal/database/meilisearch/restore.sh new file mode 100644 index 0000000..388b0d1 --- /dev/null +++ b/internal/database/meilisearch/restore.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +set -euo pipefail + +cleanup_fs() { + echo 'Cleaning up' >&2 + rm -rf data.ms.restore + if [[ -n "$dump" ]]; then + rm -f "$dump" + fi +} + +trap 'cleanup_fs' EXIT + +echo 'Uploading dump' >&2 +now="$(date +%Y%m%d-%H%M%S)" +dump="dumps/$now.dump" +cat > "$dump" + +restore="restore_${now}_data.ms" +printf 'Creating new database "%s" from dump\n' "$restore" >&2 +port="$(( RANDOM + 32767 ))" +meilisearch --import-dump "$dump" --db-path "$restore" --http-addr 127.0.0.1:"$port" & +restore_pid="$!" +while ! nc -z 127.0.0.1 "$port"; do + if ! kill -0 "$restore_pid"; then + exit 1 + fi + sleep 1 +done +echo 'Restore finished' >&2 + +echo 'Moving "data.ms" to "old_data.ms"' >&2 +rm -rf old_data.ms +mv data.ms old_data.ms +printf 'Moving "%s" to "data.ms"\n' "$restore" >&2 +mv "$restore" data.ms +trap - EXIT +cleanup_fs +echo 'Restarting Meilisearch' >&2 +kill 1