diff --git a/docs/README.md b/docs/README.md index 00e8acd4a..e0e518706 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,6 +97,7 @@ This documentation covers the installation, configuration, and usage of Dokku - - [Registry Management](advanced-usage/registry-management.md) - Docker registry integration - [Repository Management](advanced-usage/repository-management.md) - Git repository settings - [Build Tracking](advanced-usage/builds.md) - Inspect, cancel, and manage builds +- [Datastore Management](advanced-usage/datastore-management.md) - Create and link datastore services - [Deployment Tasks](advanced-usage/deployment-tasks.md) - Pre/post deploy hooks - [Event Logs](advanced-usage/event-logs.md) - Dokku event history - [Backup and Recovery](advanced-usage/backup-recovery.md) - Data backup strategies diff --git a/docs/advanced-usage/datastore-management.md b/docs/advanced-usage/datastore-management.md new file mode 100644 index 000000000..4c27f4d85 --- /dev/null +++ b/docs/advanced-usage/datastore-management.md @@ -0,0 +1,127 @@ +# Datastore Management + +``` +datastore:app-links [] # list all datastore service links for a given app +datastore:backup [--id ]... # create a backup of the datastore service to an existing s3 bucket +datastore:backup-auth ... # set up authentication for backups on the datastore service +datastore:backup-deauth [--id ] # remove backup authentication for the datastore service +datastore:backup-logs [-t|--tail []] # print the most recent output of the scheduled backups of the service +datastore:backup-schedule ... # schedule a backup of the datastore service +datastore:backup-schedule-cat [--id ] # cat the crontab line of a scheduled backup for the service +datastore:backup-set-encryption [--id ] # set encryption for all future backups of datastore service +datastore:backup-set-public-key-encryption [--id ] # set GPG Public Key encryption for all future backups of datastore service +datastore:backup-unschedule [--id |--all] # unschedule the backup of the datastore service +datastore:backup-unset-encryption [--id ] # unset encryption for future backups of the datastore service +datastore:backup-unset-public-key-encryption [--id ] # unset GPG Public Key encryption for future backups of the datastore service +datastore:clone [--clone-flags...] # create container then copy data from into +datastore:connect # connect to the service via the connection tool +datastore:create [--create-flags...] # create a datastore service +datastore:create-database # create a database in the datastore service +datastore:create-user [] # create a user in the datastore service +datastore:destroy [-f|--force] # delete the datastore service/data/container if there are no links left +datastore:destroy-database [-f|--force] # destroy a database created in the datastore service +datastore:destroy-user [-f|--force] # destroy a user created in the datastore service +datastore:enter # enter or run a command in a running datastore service container +datastore:exists # check if the datastore service exists +datastore:export [-f|--file ] [--force] # export a dump of the datastore service database +datastore:expose # expose a datastore service on custom host:port if provided (random port on the 0.0.0.0 interface if otherwise unspecified) +datastore:help [ []] # print the help for every datastore, one datastore or one of its commands +datastore:import [-f|--file ] # import a dump into the datastore service database +datastore:info [] [--info-flags...] # print the service information +datastore:label ... # add docker labels to the service container +datastore:link [] [--link-flags...] # link the datastore service to the app +datastore:linked [] # check if the datastore service is linked to an app +datastore:links # list all apps linked to the datastore service +datastore:list # list all datastore services +datastore:logs [-t|--tail []] # print the most recent log(s) for this service +datastore:mount [--replace] ... # mount a host path or docker volume into the service container +datastore:pause # pause a running datastore service +datastore:promote [] # promote service as DATABASE_URL in +datastore:reexpose # reexpose a datastore service, applying its expose settings +datastore:reset [-f|--force] # delete all data in the datastore service, keeping the service and its links +datastore:restart # graceful shutdown and restart of the datastore service container +datastore:set []... # set or clear a property for a service +datastore:set-user-password []... # change the password of a user created in the datastore service +datastore:start # start a previously stopped datastore service +datastore:stop # stop a running datastore service +datastore:unexpose # unexpose a previously exposed datastore service +datastore:unlabel [--all] [...] # remove one or all docker labels from the service container +datastore:unlink [] [-n|--no-restart] # unlink the datastore service from the app +datastore:unmount [--all] [...] # remove one or all mounts from the service container +datastore:upgrade [--upgrade-flags...] # upgrade service to the specified versions +datastore:version # print the version of dokku-datastore the plugin runs +``` + +The `datastore` plugin manages datastore services such as postgres, mysql and redis, and links them to apps. Every command takes the datastore type as its first argument, and the datastores it serves are the ones the [official datastore plugins](/docs/community/plugins.md#official-plugins) provide: clickhouse, couchdb, elasticsearch, graphite, mariadb, meilisearch, memcached, mongo, mysql, nats, omnisci, postgres, pushpin, rabbitmq, redis, rethinkdb, solr and typesense. + +The plugin is built from [dokku-datastore](https://github.com/dokku/dokku-datastore), which the official datastore plugins also run, and keeps its services in the same place they do, under `/var/lib/dokku/services`. + +## Usage + +### Creating and linking a service + +A service is created with `datastore:create` and linked to an app with `datastore:link`, which sets the variable the app connects with, such as `DATABASE_URL` for postgres or `REDIS_URL` for redis, and restarts the app. + +```shell +dokku datastore:create postgres lollipop +dokku datastore:link postgres lollipop node-js-app +``` + +`datastore:unlink` removes the link, and `datastore:destroy` removes a service once nothing is linked to it. + +```shell +dokku datastore:unlink postgres lollipop node-js-app +dokku datastore:destroy postgres lollipop +``` + +Deleting an app unlinks it from every service it is linked to. + +### Getting help + +`datastore:help` lists the datastore types and the commands they share. Not every datastore implements every command, so the help for one datastore lists only the commands it implements, including ones only it has, such as `connect-admin` for mongo. The help for one of a datastore's commands describes its flags and shows examples. + +```shell +dokku datastore:help +dokku datastore:help postgres +dokku datastore:help postgres create +``` + +### Scheduled backups + +A backup scheduled with `datastore:backup-schedule` is added to the dokku crontab as a `dokku datastore:backup` command, and is managed with the [cron plugin](/docs/processes/scheduled-cron-tasks.md) like any other scheduled task. + +```shell +dokku datastore:backup-auth postgres lollipop AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY +dokku datastore:backup-schedule postgres lollipop "0 3 * * *" my-s3-bucket +``` + +### Images + +Installing dokku does not pull the image of every datastore. A datastore's images are pulled the first time a command needs them, and on later installs and upgrades once the datastore has a service. + +### Official datastore plugins + +The official datastore plugins, such as [dokku-postgres](https://github.com/dokku/dokku-postgres), may be installed alongside the `datastore` plugin. While one is enabled, the `datastore` plugin leaves that datastore to it when dokku runs plugin triggers, such as unlinking an app that is being deleted or writing scheduled backups into the crontab. Commands for that datastore should be run through the official plugin until it is removed. + +The `datastore` plugin does not read the datastore definitions an official plugin ships, so a host running a modified official plugin should keep using it. + +#### Migrating from an official datastore plugin + +Moving a datastore from its official plugin to the `datastore` plugin leaves its services, containers, data, settings and links where they are, and does not stop running services or the apps linked to them. Using postgres as an example: + +1. Check that `dokku datastore:version` is at least the version dokku-postgres runs, which is printed by `/var/lib/dokku/data/postgres/dokku-datastore version`. If the official plugin runs a newer version, wait for a dokku release that ships it. +2. Uninstall the official plugin. + + ```shell + dokku plugin:uninstall postgres + ``` + +3. Run every plugin's install trigger, which sets the host up for postgres, brings its services up to date and rewrites the crontab so scheduled backups run through `dokku datastore:backup`. + + ```shell + dokku plugin:install + ``` + +4. Change anything that runs `dokku postgres: ` to run `dokku datastore: postgres `. + +To move back, install the official plugin again from a release running at least the version `dokku datastore:version` prints. diff --git a/docs/community/plugins.md b/docs/community/plugins.md index c738954d8..74e38428c 100644 --- a/docs/community/plugins.md +++ b/docs/community/plugins.md @@ -22,6 +22,8 @@ Let's take a quick look at the current Dokku nginx plugin that's shipped with Do The following plugins are available and provided by Dokku maintainers. Please file issues against their respective issue trackers. +Every datastore below is also served by the `datastore` plugin Dokku ships, as in `dokku datastore:create postgres lollipop`. See the [datastore management documentation](/docs/advanced-usage/datastore-management.md) for more information. + | Plugin | Author | Compatibility | | ------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | | [Clickhouse](https://github.com/dokku/dokku-clickhouse) | [dokku][] | 0.4.0+ | diff --git a/docs/template.html b/docs/template.html index d4cabbfa8..769044e4f 100644 --- a/docs/template.html +++ b/docs/template.html @@ -190,6 +190,7 @@ Backup and Recovery Build Tracking + Datastore Management Deployment Tasks Docker Container Options Docker Registry Management diff --git a/go.work b/go.work index 0a6cb68c5..48a0a82e8 100644 --- a/go.work +++ b/go.work @@ -18,6 +18,7 @@ use ( ./plugins/common ./plugins/config ./plugins/cron + ./plugins/datastore ./plugins/docker-options ./plugins/domains ./plugins/git diff --git a/plugins/datastore/.gitignore b/plugins/datastore/.gitignore new file mode 100644 index 000000000..ffa8e85d9 --- /dev/null +++ b/plugins/datastore/.gitignore @@ -0,0 +1,10 @@ +/commands +/subcommands/* +/triggers/* +/triggers +/cron-entries +/install +/post-* +/pre-* +/service-list +/update diff --git a/plugins/datastore/Makefile b/plugins/datastore/Makefile new file mode 100644 index 000000000..43ba00ef5 --- /dev/null +++ b/plugins/datastore/Makefile @@ -0,0 +1,6 @@ +SUBCOMMANDS = subcommands/app-links subcommands/backup subcommands/backup-auth subcommands/backup-deauth subcommands/backup-logs subcommands/backup-schedule subcommands/backup-schedule-cat subcommands/backup-set-encryption subcommands/backup-set-public-key-encryption subcommands/backup-unschedule subcommands/backup-unset-encryption subcommands/backup-unset-public-key-encryption subcommands/clone subcommands/connect subcommands/create subcommands/create-database subcommands/create-user subcommands/destroy subcommands/destroy-database subcommands/destroy-user subcommands/enter subcommands/exists subcommands/export subcommands/expose subcommands/import subcommands/info subcommands/label subcommands/link subcommands/linked subcommands/links subcommands/list subcommands/logs subcommands/mount subcommands/pause subcommands/promote subcommands/reexpose subcommands/reset subcommands/restart subcommands/set subcommands/set-user-password subcommands/start subcommands/stop subcommands/unexpose subcommands/unlabel subcommands/unlink subcommands/unmount subcommands/upgrade +TRIGGERS = triggers/cron-entries triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-container-create triggers/post-extract triggers/pre-build triggers/pre-delete triggers/pre-release-builder triggers/pre-restore triggers/pre-start triggers/service-list triggers/update +BUILD = commands subcommands triggers +PLUGIN_NAME = datastore + +include ../../common.mk diff --git a/plugins/datastore/datastore.go b/plugins/datastore/datastore.go new file mode 100644 index 000000000..a61582c3f --- /dev/null +++ b/plugins/datastore/datastore.go @@ -0,0 +1,5 @@ +// Package datastore is the dokku plugin serving every datastore under a single +// command, as in `dokku datastore:create postgres lollipop`. Its commands, +// subcommands and triggers are dokku-datastore's, reached through its sdk +// package. +package datastore diff --git a/plugins/datastore/datastore_test.go b/plugins/datastore/datastore_test.go new file mode 100644 index 000000000..b46f0393e --- /dev/null +++ b/plugins/datastore/datastore_test.go @@ -0,0 +1,55 @@ +package datastore + +import ( + "os" + "slices" + "strings" + "testing" + + "github.com/dokku/dokku-datastore/sdk" +) + +// makefileList returns the names a Makefile variable links, without the +// directory each is listed under +func makefileList(t *testing.T, variable string) []string { + t.Helper() + + contents, err := os.ReadFile("Makefile") + if err != nil { + t.Fatalf("unable to read the Makefile: %s", err) + } + + for _, line := range strings.Split(string(contents), "\n") { + value, found := strings.CutPrefix(line, variable+" = ") + if !found { + continue + } + + names := []string{} + for _, entry := range strings.Fields(value) { + names = append(names, entry[strings.Index(entry, "/")+1:]) + } + + slices.Sort(names) + return names + } + + t.Fatalf("the Makefile does not set %s", variable) + return nil +} + +// Every command a datastore may implement needs a subcommand linked, or dokku +// has nothing to run when it is asked for +func TestEverySubcommandIsLinked(t *testing.T) { + if linked, expected := makefileList(t, "SUBCOMMANDS"), sdk.SubcommandNames(); !slices.Equal(linked, expected) { + t.Errorf("expected the Makefile to link the subcommands %v, got %v", expected, linked) + } +} + +// Every trigger a datastore implements needs a trigger linked, or dokku never +// runs it +func TestEveryTriggerIsLinked(t *testing.T) { + if linked, expected := makefileList(t, "TRIGGERS"), sdk.TriggerNames(); !slices.Equal(linked, expected) { + t.Errorf("expected the Makefile to link the triggers %v, got %v", expected, linked) + } +} diff --git a/plugins/datastore/go.mod b/plugins/datastore/go.mod new file mode 100644 index 000000000..451e9832e --- /dev/null +++ b/plugins/datastore/go.mod @@ -0,0 +1,78 @@ +module github.com/dokku/dokku/plugins/datastore + +go 1.27.1 + +require github.com/dokku/dokku-datastore v0.0.0-20261030002556-16e62ce11bc6 + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alexellis/go-execute/v2 v2.2.1 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/bgentry/speakeasy v0.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/compose-spec/compose-go/v2 v2.15.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.8.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dokku/docker-port-forward v0.0.0-20260927082313-8cbfc680c944 // indirect + github.com/dokku/dokku/plugins/common v0.0.0-20260914154134-61ff696e0012 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/josegonzalez/cli-skeleton v0.25.0 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/melbahja/goph v1.5.2 // indirect + github.com/mitchellh/cli v1.1.5 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.56.0 // indirect + github.com/moby/moby/client v0.6.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/otiai10/copy v1.14.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.11 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rs/zerolog v1.35.1 // indirect + github.com/ryanuber/columnize v2.1.2+incompatible // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.10.1 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + mvdan.cc/sh/v3 v3.14.1 // indirect +) + +replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/datastore/go.sum b/plugins/datastore/go.sum new file mode 100644 index 000000000..573ec3045 --- /dev/null +++ b/plugins/datastore/go.sum @@ -0,0 +1,209 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alexellis/go-execute/v2 v2.2.1 h1:4Ye3jiCKQarstODOEmqDSRCqxMHLkC92Bhse743RdOI= +github.com/alexellis/go-execute/v2 v2.2.1/go.mod h1:FMdRnUTiFAmYXcv23txrp3VYZfLo24nMpiIneWgKHTQ= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= +github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00= +github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= +github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dokku/docker-port-forward v0.0.0-20260927082313-8cbfc680c944 h1:3CQ4ktARNTVKDuzT19ZWmAD2VQ1W1GVJ7oMfyrUg/FI= +github.com/dokku/docker-port-forward v0.0.0-20260927082313-8cbfc680c944/go.mod h1:RFL0Qy08FjSqhCJECTTLhUIPDjBnQCHCWX9+NNegNIg= +github.com/dokku/dokku-datastore v0.0.0-20261030002556-16e62ce11bc6 h1:+hkZUQal2lNTXPceLVqUCmTqTdoGvmccHqoR9UtsBPk= +github.com/dokku/dokku-datastore v0.0.0-20261030002556-16e62ce11bc6/go.mod h1:B6WoAGl/Q8VD23tOJqvHBn90muRMmEF5v9tCRNVWXSI= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= +github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/josegonzalez/cli-skeleton v0.25.0 h1:pxBBuAIO7ALvfXrhnvmaTvo1cWv34RKONccEpw+2UN4= +github.com/josegonzalez/cli-skeleton v0.25.0/go.mod h1:eFc5CRWq4w3NwRRt4pJw85IEV/Bp4H+aIFQ50e6sYFY= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/melbahja/goph v1.5.2 h1:2eoR45SLF3LyM6tnIhnpjakvXTjtQMJqOK/mp1PYojM= +github.com/melbahja/goph v1.5.2/go.mod h1:T+5uoB1PDP6EeK2qXerf5gRh7b6IF8u37GK2ckEi9FU= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= +github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= +github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= +github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= +github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE= +github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +mvdan.cc/sh/v3 v3.14.1 h1:bXkhQWNHCs0KZEChF8hYS6FC+T2N9mUZLbQv9blditI= +mvdan.cc/sh/v3 v3.14.1/go.mod h1:syYCoFET8w9tvevxiXUtY8/ICrU+l26jHmhJDra3Vwo= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/plugins/datastore/plugin.toml b/plugins/datastore/plugin.toml new file mode 100644 index 000000000..2c7aed34a --- /dev/null +++ b/plugins/datastore/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core datastore plugin" +version = "0.38.33" +[plugin.config] diff --git a/plugins/datastore/src/commands/commands.go b/plugins/datastore/src/commands/commands.go new file mode 100644 index 000000000..4e5ba2784 --- /dev/null +++ b/plugins/datastore/src/commands/commands.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/dokku/dokku-datastore/sdk" +) + +// main answers the help dokku asks every plugin for, and runs any datastore +// command dokku found no subcommand for, which is what a datastore's own +// commands are +func main() { + os.Exit(sdk.RunCommands(os.Args[1:])) +} diff --git a/plugins/datastore/src/subcommands/subcommands.go b/plugins/datastore/src/subcommands/subcommands.go new file mode 100644 index 000000000..bad928ea0 --- /dev/null +++ b/plugins/datastore/src/subcommands/subcommands.go @@ -0,0 +1,21 @@ +package main + +import ( + "os" + "strings" + + "github.com/dokku/dokku-datastore/sdk" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + args := []string{} + if len(os.Args) > 2 { + args = os.Args[2:] + } + + os.Exit(sdk.RunSubcommand(subcommand, args)) +} diff --git a/plugins/datastore/src/triggers/triggers.go b/plugins/datastore/src/triggers/triggers.go new file mode 100644 index 000000000..a502bb846 --- /dev/null +++ b/plugins/datastore/src/triggers/triggers.go @@ -0,0 +1,16 @@ +package main + +import ( + "os" + "strings" + + "github.com/dokku/dokku-datastore/sdk" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + + os.Exit(sdk.RunTrigger(trigger, os.Args[1:])) +} diff --git a/tests.mk b/tests.mk index ff23614f5..da1230b1a 100644 --- a/tests.mk +++ b/tests.mk @@ -182,6 +182,7 @@ go-tests: @$(MAKE) go-test-plugin PLUGIN_NAME=common @$(MAKE) go-test-plugin PLUGIN_NAME=config @$(MAKE) go-test-plugin PLUGIN_NAME=cron + @$(MAKE) go-test-plugin PLUGIN_NAME=datastore @$(MAKE) go-test-plugin PLUGIN_NAME=docker-options @$(MAKE) go-test-plugin PLUGIN_NAME=logs @$(MAKE) go-test-plugin PLUGIN_NAME=network diff --git a/tests/unit/datastore.bats b/tests/unit/datastore.bats new file mode 100644 index 000000000..2c9e197b7 --- /dev/null +++ b/tests/unit/datastore.bats @@ -0,0 +1,137 @@ +#!/usr/bin/env bats + +load test_helper + +TEST_SERVICE="ds-test-service" + +setup() { + global_setup + create_app +} + +teardown() { + dokku datastore:unlink redis "$TEST_SERVICE" "$TEST_APP" --no-restart >/dev/null 2>&1 || true + dokku datastore:destroy redis "$TEST_SERVICE" --force >/dev/null 2>&1 || true + destroy_app + global_teardown +} + +@test "(datastore) datastore:help" { + run /bin/bash -c "dokku datastore" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Manage datastore services." + assert_output_contains "datastore:create " + help_output="$output" + + run /bin/bash -c "dokku datastore:help" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "$help_output" + + run /bin/bash -c "dokku datastore:help redis" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku datastore:list redis" + + run /bin/bash -c "dokku datastore:help redis create" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku datastore:create redis " + + run /bin/bash -c "dokku datastore:help nonsense" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku help --all" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "datastore:create " +} + +@test "(datastore) datastore:version" { + run /bin/bash -c "dokku datastore:version" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "v" +} + +@test "(datastore) create, link, unlink and destroy a service" { + run /bin/bash -c "dokku datastore:create redis $TEST_SERVICE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku datastore:exists redis $TEST_SERVICE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku datastore:list redis" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "$TEST_SERVICE" + + run /bin/bash -c "dokku datastore:link redis $TEST_SERVICE $TEST_APP --no-restart" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:get $TEST_APP REDIS_URL" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "redis://" + + run /bin/bash -c "dokku datastore:unlink redis $TEST_SERVICE $TEST_APP --no-restart" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:get $TEST_APP REDIS_URL" + echo "output: $output" + echo "status: $status" + assert_output "" + + run /bin/bash -c "dokku datastore:destroy redis $TEST_SERVICE --force" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku datastore:exists redis $TEST_SERVICE" + echo "output: $output" + echo "status: $status" + assert_failure +} + +@test "(datastore) deleting an app unlinks its services" { + run /bin/bash -c "dokku datastore:create redis $TEST_SERVICE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku datastore:link redis $TEST_SERVICE $TEST_APP --no-restart" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku --force apps:destroy $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku datastore:linked redis $TEST_SERVICE $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_failure + + create_app +}