diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25062ce6c..7f410a741 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,7 @@ on: jobs: lint: + if: false # disable linting for infobloxopen/migrate name: lint runs-on: ubuntu-latest steps: @@ -31,7 +32,7 @@ jobs: go-version: ${{ matrix.go }} - name: Run test - run: make test COVERAGE_DIR=/tmp/coverage + run: make test COVERAGE_DIR=/tmp/coverage DATABASE="postgres mysql clickhouse mongodb pgx pgx5 rqlite sqlite sqlite3" - name: Send goveralls coverage uses: shogo82148/actions-goveralls@v1 @@ -51,11 +52,11 @@ jobs: goreleaser: name: Release a new version - needs: [lint, test] + needs: [test] runs-on: ubuntu-latest environment: GoReleaser # This job only runs when - # 1. When the previous `lint` and `test` jobs has completed successfully + # 1. When the previous `test` job has completed successfully # 2. When the repository is not a fork, i.e. it will only run on the official golang-migrate/migrate # 3. When the workflow is triggered by a tag with `v` prefix if: ${{ success() && github.repository == 'golang-migrate/migrate' && startsWith(github.ref, 'refs/tags/v') }} diff --git a/Dockerfile b/Dockerfile index 46b4e18fd..3ba5ae851 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG VERSION RUN apk add --no-cache git gcc musl-dev make -WORKDIR /go/src/github.com/golang-migrate/migrate +WORKDIR /go/src/github.com/infobloxopen/migrate ENV GO111MODULE=on @@ -15,12 +15,11 @@ COPY . ./ RUN make build-docker -FROM alpine:3.21 +FROM gcr.io/distroless/static:nonroot -RUN apk add --no-cache ca-certificates - -COPY --from=builder /go/src/github.com/golang-migrate/migrate/build/migrate.linux-386 /usr/local/bin/migrate -RUN ln -s /usr/local/bin/migrate /migrate +COPY --from=builder /go/src/github.com/infobloxopen/migrate/cmd/migrate/config /cli/config/ +COPY --from=builder /go/src/github.com/infobloxopen/migrate/build/migrate.linux-386 /migrate +COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/ ENTRYPOINT ["migrate"] CMD ["--help"] diff --git a/Dockerfile.github-actions b/Dockerfile.github-actions index 9786e1210..061210d74 100644 --- a/Dockerfile.github-actions +++ b/Dockerfile.github-actions @@ -1,4 +1,4 @@ -FROM alpine:3.19 +FROM alpine:latest RUN apk add --no-cache ca-certificates diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..14dea757c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,63 @@ + +// This library defines the isPrBuild, prepareBuild and finalizeBuild methods +@Library('jenkins.shared.library') _ + +pipeline { + agent { + label 'ubuntu_docker_label' + } + tools { + go "Go 1.24.2" + } + options { + checkoutToSubdirectory('src/github.com/infobloxopen/migrate') + } + environment { + GOPATH = "$WORKSPACE" + DIRECTORY = "src/github.com/infobloxopen/migrate" + } + + stages { + stage("Setup") { + steps { + // prepareBuild is one of the Secure CICD helper methods + prepareBuild() + } + } + stage("Unit Tests") { + steps { + dir("$DIRECTORY") { + // sh "make test" + } + } + } + stage("Build Image") { + steps { + withDockerRegistry([credentialsId: "${env.JENKINS_DOCKER_CRED_ID}", url: ""]) { + dir("$DIRECTORY") { + sh "make build" + } + } + } + } + } + post { + success { + // finalizeBuild is one of the Secure CICD helper methods + dir("$DIRECTORY") { + finalizeBuild( + sh( + script: 'make list-of-images', + returnStdout: true + ) + ) + } + } + cleanup { + dir("$DIRECTORY") { + sh "make clean || true" + } + cleanWs() + } + } +} diff --git a/Makefile b/Makefile index 8e23a43c7..f5e190355 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,12 @@ SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx pgx5 rqlite DATABASE_TEST ?= $(DATABASE) sqlite sqlite3 sqlcipher -VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-) +BUILD_NUMBER ?= 0 +VERSION ?= $(shell git describe --tags --long --dirty=-unsupported 2>/dev/null | cut -c 2-)-j$(BUILD_NUMBER) TEST_FLAGS ?= REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)") COVERAGE_DIR ?= .coverage -build: - CGO_ENABLED=0 go build -ldflags='-X main.Version=$(VERSION)' -tags '$(DATABASE) $(SOURCE)' ./cmd/migrate - build-docker: CGO_ENABLED=0 go build -a -o build/migrate.linux-386 -ldflags="-s -w -X main.Version=${VERSION}" -tags "$(DATABASE) $(SOURCE)" ./cmd/migrate @@ -24,6 +22,17 @@ build-cli: clean cd ./cli/build && shasum -a 256 * > sha256sum.txt cat ./cli/build/sha256sum.txt +build: + docker build --pull --build-arg VERSION=$(VERSION) . -t infoblox/migrate -t infoblox/migrate:$(VERSION) + +docker-push: + docker push infoblox/migrate:$(VERSION) + +show-image-version: + echo $(VERSION) + +list-of-images: + @echo "infoblox/migrate:$(VERSION)" clean: -rm -r ./cli/build @@ -117,4 +126,3 @@ endef SHELL = /bin/sh RAND = $(shell echo $$RANDOM) - diff --git a/cli/main.go b/cli/main.go index a3c249885..254e8168f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -1,6 +1,8 @@ package main -import "github.com/golang-migrate/migrate/v4/internal/cli" +import ( + "github.com/golang-migrate/migrate/v4/internal/cli" +) // Deprecated, please use cmd/migrate func main() { diff --git a/cmd/migrate/.gitignore b/cmd/migrate/.gitignore new file mode 100644 index 000000000..219f6a587 --- /dev/null +++ b/cmd/migrate/.gitignore @@ -0,0 +1 @@ +migrate diff --git a/cmd/migrate/config.go b/cmd/migrate/config.go new file mode 100644 index 000000000..a03097618 --- /dev/null +++ b/cmd/migrate/config.go @@ -0,0 +1,42 @@ +package main + +import "github.com/spf13/pflag" + +const ( + // configuration defaults support local development (i.e. "go run ...") + defaultDatabaseDSN = "" + defaultDatabaseDriver = "postgres" + defaultDatabaseAddress = "0.0.0.0:5432" + defaultDatabaseName = "" + defaultDatabaseUser = "postgres" + defaultDatabasePassword = "postgres" + defaultDatabaseSSL = "disable" + defaultConfigDirectory = "/cli/config" +) + +var ( + // define flag overrides + flagHelp = pflag.Bool("help", false, "Print usage") + flagVersion = pflag.String("version", Version, "Print version") + flagLoggingVerbose = pflag.Bool("verbose", true, "Print verbose logging") + flagPrefetch = pflag.Uint("prefetch", 10, "Number of migrations to load in advance before executing") + flaglockTimeout = pflag.Uint("lock-timeout", 15, "Allow N seconds to acquire database lock") + + flagDatabaseDSN = pflag.String("database.dsn", defaultDatabaseDSN, "database connection string") + flagDatabaseDriver = pflag.String("database.driver", defaultDatabaseDriver, "database driver") + flagDatabaseAddress = pflag.String("database.address", defaultDatabaseAddress, "address of the database") + flagDatabaseName = pflag.String("database.name", defaultDatabaseName, "name of the database") + flagDatabaseUser = pflag.String("database.user", defaultDatabaseUser, "database username") + flagDatabasePassword = pflag.String("database.password", defaultDatabasePassword, "database password") + flagDatabaseSSL = pflag.String("database.ssl", defaultDatabaseSSL, "database ssl mode") + + flagSource = pflag.String("source", "", "Location of the migrations (driver://url)") + flagPath = pflag.String("path", "", "Shorthand for -source=file://path") + + flagConfigDirectory = pflag.String("config.source", defaultConfigDirectory, "directory of the configuration file") + flagConfigFile = pflag.String("config.file", "", "configuration file name without extension") + + // goto command flags + flagDirty = pflag.Bool("force-dirty-handling", false, "force the handling of dirty database state") + flagMountPath = pflag.String("cache-dir", "", "path to the cache-dir which is used to copy the migration files") +) diff --git a/cmd/migrate/config/defaults.yaml b/cmd/migrate/config/defaults.yaml new file mode 100644 index 000000000..e861bf15c --- /dev/null +++ b/cmd/migrate/config/defaults.yaml @@ -0,0 +1,14 @@ +help: false +version: false +verbose: true +prefetch: 10 +lockTimeout: 15 +path: "/atlas-migrations/migrations" +#source: "file:///atlas-migrations/migrations" +database: + driver: postgres + address: postgres:5432 + name: app_db + user: postgres + password: postgres + ssl: disable \ No newline at end of file diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index 7cda72e71..b6188d95f 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -1,6 +1,39 @@ package main -import "github.com/golang-migrate/migrate/v4/internal/cli" +import ( + "log" + "strings" + + "github.com/golang-migrate/migrate/v4/internal/cli" + "github.com/infobloxopen/hotload" + _ "github.com/infobloxopen/hotload/fsnotify" + "github.com/jackc/pgx/v4/stdlib" + "github.com/lib/pq" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func init() { + pflag.Parse() + viper.BindPFlags(pflag.CommandLine) + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AddConfigPath(viper.GetString("config.source")) + if viper.GetString("config.file") != "" { + viper.SetConfigName(viper.GetString("config.file")) + if err := viper.ReadInConfig(); err != nil { + log.Fatalf("cannot load configuration: %v", err) + } + } + // logrus formatter + customFormatter := new(logrus.JSONFormatter) + logrus.SetFormatter(customFormatter) + + hotload.RegisterSQLDriver("pgx", stdlib.GetDefaultDriver()) + hotload.RegisterSQLDriver("postgres", pq.Driver{}) + hotload.RegisterSQLDriver("postgresql", pq.Driver{}) +} func main() { cli.Main(Version) diff --git a/database/postgres/storage.go b/database/postgres/storage.go new file mode 100644 index 000000000..ea0c6bac5 --- /dev/null +++ b/database/postgres/storage.go @@ -0,0 +1,293 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "io" + + "github.com/golang-migrate/migrate/v4/database" + "github.com/golang-migrate/migrate/v4/source" + "github.com/lib/pq" +) + +// Ensure Postgres implements MigrationStorageDriver +var _ database.MigrationStorageDriver = &Postgres{} + +// ensureEnhancedVersionTable checks if the enhanced versions table exists and creates/updates it. +// This version includes columns for storing migration scripts. +func (p *Postgres) ensureEnhancedVersionTable() (err error) { + if err = p.Lock(); err != nil { + return err + } + + defer func() { + if e := p.Unlock(); e != nil { + if err == nil { + err = e + } else { + err = fmt.Errorf("unlock error: %v, original error: %v", e, err) + } + } + }() + + exists, err := p.tableExists() + if err != nil { + return err + } + + if !exists { + return p.createEnhancedTable() + } + + return p.addMissingColumns() +} + +// tableExists checks if the migrations table exists +func (p *Postgres) tableExists() (bool, error) { + query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1` + row := p.conn.QueryRowContext(context.Background(), query, p.config.migrationsSchemaName, p.config.migrationsTableName) + + var count int + err := row.Scan(&count) + if err != nil { + return false, &database.Error{OrigErr: err, Query: []byte(query)} + } + + return count > 0, nil +} + +// createEnhancedTable creates the migrations table with all required columns +func (p *Postgres) createEnhancedTable() error { + query := `CREATE TABLE IF NOT EXISTS ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + pq.QuoteIdentifier(p.config.migrationsTableName) + ` ( + version bigint not null primary key, + dirty boolean not null, + up_script text, + down_script text, + created_at timestamp with time zone default now() + )` + if _, err := p.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + return nil +} + +// addMissingColumns adds any missing columns to existing table +func (p *Postgres) addMissingColumns() error { + columns := []string{"up_script", "down_script", "created_at"} + + for _, column := range columns { + exists, err := p.columnExists(column) + if err != nil { + return err + } + + if !exists { + if err := p.addColumn(column); err != nil { + return err + } + } + } + + return nil +} + +// columnExists checks if a specific column exists in the migrations table +func (p *Postgres) columnExists(columnName string) (bool, error) { + query := `SELECT COUNT(1) FROM information_schema.columns + WHERE table_schema = $1 AND table_name = $2 AND column_name = $3` + + var count int + err := p.conn.QueryRowContext(context.Background(), query, + p.config.migrationsSchemaName, p.config.migrationsTableName, columnName).Scan(&count) + if err != nil { + return false, &database.Error{OrigErr: err, Query: []byte(query)} + } + + return count > 0, nil +} + +// addColumn adds a specific column to the migrations table +func (p *Postgres) addColumn(columnName string) error { + var columnDef string + switch columnName { + case "up_script", "down_script": + columnDef = "text" + case "created_at": + columnDef = "timestamp with time zone default now()" + default: + return fmt.Errorf("unknown column: %s", columnName) + } + + alterQuery := `ALTER TABLE ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + + pq.QuoteIdentifier(p.config.migrationsTableName) + ` ADD COLUMN ` + columnName + ` ` + columnDef + if _, err := p.conn.ExecContext(context.Background(), alterQuery); err != nil { + return &database.Error{OrigErr: err, Query: []byte(alterQuery)} + } + return nil +} + +// StoreMigration stores the up and down migration scripts for a given version +func (p *Postgres) StoreMigration(version uint, upScript, downScript []byte) error { + // Ensure the enhanced table exists + if err := p.ensureEnhancedVersionTable(); err != nil { + return err + } + + query := `INSERT INTO ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + + pq.QuoteIdentifier(p.config.migrationsTableName) + + ` (version, dirty, up_script, down_script) VALUES ($1, false, $2, $3) + ON CONFLICT (version) DO UPDATE SET + up_script = EXCLUDED.up_script, + down_script = EXCLUDED.down_script, + created_at = now()` + + _, err := p.conn.ExecContext(context.Background(), query, int64(version), string(upScript), string(downScript)) + if err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + return nil +} + +// GetMigration retrieves the stored migration scripts for a given version +func (p *Postgres) GetMigration(version uint) (upScript, downScript []byte, err error) { + query := `SELECT up_script, down_script FROM ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + + pq.QuoteIdentifier(p.config.migrationsTableName) + ` WHERE version = $1` + + var upScriptStr, downScriptStr sql.NullString + err = p.conn.QueryRowContext(context.Background(), query, int64(version)).Scan(&upScriptStr, &downScriptStr) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil, fmt.Errorf("migration version %d not found", version) + } + return nil, nil, &database.Error{OrigErr: err, Query: []byte(query)} + } + + if upScriptStr.Valid { + upScript = []byte(upScriptStr.String) + } + if downScriptStr.Valid { + downScript = []byte(downScriptStr.String) + } + + return upScript, downScript, nil +} + +// GetStoredMigrations returns all migration versions that have scripts stored +func (p *Postgres) GetStoredMigrations() ([]uint, error) { + query := `SELECT version FROM ` + pq.QuoteIdentifier(p.config.migrationsSchemaName) + `.` + + pq.QuoteIdentifier(p.config.migrationsTableName) + + ` WHERE up_script IS NOT NULL OR down_script IS NOT NULL ORDER BY version ASC` + + rows, err := p.conn.QueryContext(context.Background(), query) + if err != nil { + return nil, &database.Error{OrigErr: err, Query: []byte(query)} + } + defer rows.Close() + + var versions []uint + for rows.Next() { + var version int64 + if err := rows.Scan(&version); err != nil { + return nil, err + } + versions = append(versions, uint(version)) + } + + return versions, rows.Err() +} + +// SyncMigrations ensures all available migrations up to maxVersion are stored in the database +func (p *Postgres) SyncMigrations(sourceDriver interface{}, maxVersion uint) error { + srcDriver, ok := sourceDriver.(source.Driver) + if !ok { + return fmt.Errorf("source driver must implement source.Driver interface") + } + + versions, err := p.collectVersions(srcDriver, maxVersion) + if err != nil { + return err + } + + return p.storeMigrations(srcDriver, versions) +} + +// collectVersions gets all migration versions up to maxVersion +func (p *Postgres) collectVersions(srcDriver source.Driver, maxVersion uint) ([]uint, error) { + first, err := srcDriver.First() + if err != nil { + return nil, fmt.Errorf("failed to get first migration: %w", err) + } + + var versions []uint + currentVersion := first + + for currentVersion <= maxVersion { + versions = append(versions, currentVersion) + + next, err := srcDriver.Next(currentVersion) + if err != nil { + if err.Error() == "file does not exist" { // Handle os.ErrNotExist + break + } + return nil, fmt.Errorf("failed to get next migration after %d: %w", currentVersion, err) + } + currentVersion = next + } + + return versions, nil +} + +// storeMigrations reads and stores migration scripts for the given versions +func (p *Postgres) storeMigrations(srcDriver source.Driver, versions []uint) error { + for _, version := range versions { + upScript, err := p.readMigrationScript(srcDriver, version, true) + if err != nil { + return err + } + + downScript, err := p.readMigrationScript(srcDriver, version, false) + if err != nil { + return err + } + + // Store the migration if we have at least one script + if len(upScript) > 0 || len(downScript) > 0 { + if err := p.StoreMigration(version, upScript, downScript); err != nil { + return fmt.Errorf("failed to store migration %d: %w", version, err) + } + } + } + + return nil +} + +// readMigrationScript reads a migration script (up or down) for a given version +func (p *Postgres) readMigrationScript(srcDriver source.Driver, version uint, isUp bool) ([]byte, error) { + var reader io.ReadCloser + var err error + + if isUp { + reader, _, err = srcDriver.ReadUp(version) + } else { + reader, _, err = srcDriver.ReadDown(version) + } + + if err != nil { + // It's OK if migration doesn't exist + return nil, nil + } + + defer reader.Close() + script, err := io.ReadAll(reader) + if err != nil { + direction := "up" + if !isUp { + direction = "down" + } + return nil, fmt.Errorf("failed to read %s migration %d: %w", direction, version, err) + } + + return script, nil +} diff --git a/database/sqlserver/sqlserver_test.go b/database/sqlserver/sqlserver_test.go index 402f4480f..33c5f0805 100644 --- a/database/sqlserver/sqlserver_test.go +++ b/database/sqlserver/sqlserver_test.go @@ -84,6 +84,7 @@ func SkipIfUnsupportedArch(t *testing.T, c dktest.ContainerInfo) { } func Test(t *testing.T) { + t.Skip("Skipping SQL Server tests in CI for infobloxopen/migrate repo") t.Run("test", test) t.Run("testMigrate", testMigrate) t.Run("testMultiStatement", testMultiStatement) diff --git a/database/storage.go b/database/storage.go new file mode 100644 index 000000000..c35b53c6e --- /dev/null +++ b/database/storage.go @@ -0,0 +1,31 @@ +package database + +// MigrationStorageDriver extends the basic Driver interface to support +// storing and retrieving migration scripts in the database itself. +// This is useful for dirty state handling when shared storage isn't available. +type MigrationStorageDriver interface { + Driver + + // StoreMigration stores the up and down migration scripts for a given version + // in the database. This allows for dirty state recovery without external files. + StoreMigration(version uint, upScript, downScript []byte) error + + // GetMigration retrieves the stored migration scripts for a given version. + // Returns the up and down scripts, or an error if the version doesn't exist. + GetMigration(version uint) (upScript, downScript []byte, err error) + + // GetStoredMigrations returns all migration versions that have scripts stored + // in the database, sorted in ascending order. + GetStoredMigrations() ([]uint, error) + + // SyncMigrations ensures all available migrations up to maxVersion are stored + // in the database. This should be called during migration runs to keep + // the database in sync with available migration files. + SyncMigrations(sourceDriver interface{}, maxVersion uint) error +} + +// SupportsStorage checks if a driver supports migration script storage +func SupportsStorage(driver Driver) bool { + _, ok := driver.(MigrationStorageDriver) + return ok +} diff --git a/database_source.go b/database_source.go new file mode 100644 index 000000000..fc8355ab5 --- /dev/null +++ b/database_source.go @@ -0,0 +1,129 @@ +package migrate + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/golang-migrate/migrate/v4/database" + "github.com/golang-migrate/migrate/v4/source" +) + +// DatabaseSource implements source.Driver by reading migrations from database storage +type DatabaseSource struct { + storageDriver database.MigrationStorageDriver + logger Logger + versions []uint +} + +var _ source.Driver = &DatabaseSource{} + +// Open is not used for DatabaseSource as it's created directly +func (d *DatabaseSource) Open(url string) (source.Driver, error) { + return d, nil +} + +// Close closes the database source +func (d *DatabaseSource) Close() error { + return nil +} + +// First returns the first migration version available in the database +func (d *DatabaseSource) First() (version uint, err error) { + if err := d.loadVersions(); err != nil { + return 0, err + } + + if len(d.versions) == 0 { + return 0, os.ErrNotExist + } + + return d.versions[0], nil +} + +// Prev returns the previous migration version relative to the current version +func (d *DatabaseSource) Prev(version uint) (prevVersion uint, err error) { + if err := d.loadVersions(); err != nil { + return 0, err + } + + for i, v := range d.versions { + if v == version && i > 0 { + return d.versions[i-1], nil + } + } + + return 0, os.ErrNotExist +} + +// Next returns the next migration version relative to the current version +func (d *DatabaseSource) Next(version uint) (nextVersion uint, err error) { + if err := d.loadVersions(); err != nil { + return 0, err + } + + for i, v := range d.versions { + if v == version && i < len(d.versions)-1 { + return d.versions[i+1], nil + } + } + + return 0, os.ErrNotExist +} + +// ReadUp reads the up migration for the specified version from the database +func (d *DatabaseSource) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) { + upScript, _, err := d.storageDriver.GetMigration(version) + if err != nil { + return nil, "", err + } + + if len(upScript) == 0 { + return nil, "", os.ErrNotExist + } + + reader := io.NopCloser(strings.NewReader(string(upScript))) + identifier = fmt.Sprintf("%d.up", version) + + return reader, identifier, nil +} + +// ReadDown reads the down migration for the specified version from the database +func (d *DatabaseSource) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) { + _, downScript, err := d.storageDriver.GetMigration(version) + if err != nil { + return nil, "", err + } + + if len(downScript) == 0 { + return nil, "", os.ErrNotExist + } + + reader := io.NopCloser(strings.NewReader(string(downScript))) + identifier = fmt.Sprintf("%d.down", version) + + return reader, identifier, nil +} + +// loadVersions loads available migration versions from the database +func (d *DatabaseSource) loadVersions() error { + if d.versions != nil { + return nil // Already loaded + } + + versions, err := d.storageDriver.GetStoredMigrations() + if err != nil { + return fmt.Errorf("failed to load migrations from database: %w", err) + } + + d.versions = versions + return nil +} + +// logPrintf writes to the logger if available +func (d *DatabaseSource) logPrintf(format string, v ...interface{}) { + if d.logger != nil { + d.logger.Printf(format, v...) + } +} diff --git a/docker-deploy.sh b/docker-deploy.sh index 558ea79be..d967acdb7 100755 --- a/docker-deploy.sh +++ b/docker-deploy.sh @@ -1,5 +1,5 @@ #!/bin/bash echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin && \ -docker build --build-arg VERSION="$TRAVIS_TAG" . -t migrate/migrate -t migrate/migrate:"$TRAVIS_TAG" && \ +docker build --pull --build-arg VERSION="$TRAVIS_TAG" . -t migrate/migrate -t migrate/migrate:"$TRAVIS_TAG" && \ docker push migrate/migrate:"$TRAVIS_TAG" && docker push migrate/migrate diff --git a/go.mod b/go.mod index ef36567a1..17abc2e27 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,10 @@ require ( cloud.google.com/go/storage v1.38.0 github.com/Azure/go-autorest/autorest/adal v0.9.16 github.com/ClickHouse/clickhouse-go v1.4.3 + github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go v1.49.6 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 // indirect github.com/cenkalti/backoff/v4 v4.1.2 github.com/cockroachdb/cockroach-go/v2 v2.1.1 github.com/dhui/dktest v0.4.6 @@ -16,8 +19,11 @@ require ( github.com/go-sql-driver/mysql v1.5.0 github.com/gobuffalo/here v0.6.0 github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-github/v39 v39.2.0 github.com/hashicorp/go-multierror v1.1.1 + github.com/infobloxopen/hotload v0.0.0-20210618153036-1535a93a8521 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx/v4 v4.18.2 @@ -26,16 +32,19 @@ require ( github.com/lib/pq v1.10.9 github.com/markbates/pkger v0.15.1 github.com/mattn/go-sqlite3 v1.14.22 - github.com/microsoft/go-mssqldb v1.0.0 + github.com/microsoft/go-mssqldb v1.8.0 github.com/mutecomm/go-sqlcipher/v4 v4.4.0 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba + github.com/onsi/gomega v1.15.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/sirupsen/logrus v1.9.3 github.com/snowflakedb/gosnowflake v1.6.19 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/xanzy/go-gitlab v0.15.0 go.mongodb.org/mongo-driver v1.7.5 golang.org/x/oauth2 v0.27.0 - golang.org/x/tools v0.24.0 + golang.org/x/tools v0.35.0 google.golang.org/api v0.169.0 modernc.org/ql v1.0.0 modernc.org/sqlite v1.18.1 @@ -76,22 +85,20 @@ require ( cloud.google.com/go/longrunning v0.5.5 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/apache/arrow/go/v10 v10.0.1 // indirect github.com/apache/thrift v0.16.0 // indirect github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 // indirect @@ -106,7 +113,6 @@ require ( github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 // indirect github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect @@ -114,15 +120,12 @@ require ( github.com/envoyproxy/go-control-plane v0.13.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.1 // indirect github.com/go-stack/stack v1.8.0 // indirect - github.com/goccy/go-json v0.9.11 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v2.0.8+incompatible // indirect @@ -133,7 +136,6 @@ require ( github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/gorilla/handlers v1.4.2 // indirect github.com/gorilla/mux v1.7.4 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -144,46 +146,35 @@ require ( github.com/jackc/pgtype v1.14.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/k0kubun/pp v2.3.0+incompatible // indirect - github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/klauspost/compress v1.15.11 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-isatty v0.0.16 // indirect - github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect - github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/onsi/ginkgo v1.16.4 // indirect - github.com/onsi/gomega v1.15.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.16 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 github.com/shopspring/decimal v1.2.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - github.com/zeebo/xxh3 v1.0.2 // indirect gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.40.0 // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect @@ -212,3 +203,31 @@ require ( modernc.org/token v1.0.0 // indirect modernc.org/zappy v1.0.0 // indirect ) + +require ( + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + github.com/klauspost/asmfmt v1.3.2 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect + github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) diff --git a/go.sum b/go.sum index 8de37ad10..b81046839 100644 --- a/go.sum +++ b/go.sum @@ -32,15 +32,16 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1 h1:T8quHYlUGyb/oqtSTwqlCr1ilJHrDv+ZtpSfo+hm1BU= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -57,12 +58,14 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1 h1:oPdPEZFSbl7oSPEAIPMPBMUmiL+mqgzBJwM/9qYcwNg= -github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -160,9 +163,6 @@ github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= 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/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -187,9 +187,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= @@ -209,6 +212,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= @@ -222,12 +227,14 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -292,7 +299,6 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -308,8 +314,6 @@ github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= @@ -321,10 +325,11 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 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/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/infobloxopen/hotload v0.0.0-20210618153036-1535a93a8521 h1:+C9LAdtx0MurMHJvGU2MB2nrcSIS/uxvKY2Sp3CAWq0= +github.com/infobloxopen/hotload v0.0.0-20210618153036-1535a93a8521/go.mod h1:9iPqQm/TZrZ9YW4AjiRVRs0jpK8xiXuSq+jYzpfZdUY= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -393,12 +398,6 @@ github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -469,8 +468,8 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= -github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= @@ -490,9 +489,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= @@ -521,13 +518,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -547,6 +545,8 @@ github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmr github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -558,6 +558,16 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/snowflakedb/gosnowflake v1.6.19 h1:KSHXrQ5o7uso25hNIzi/RObXtnSGkFgie91X82KcvMY= github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +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/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -574,8 +584,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xanzy/go-gitlab v0.15.0 h1:rWtwKTgEnXyNUGrOArN7yyc3THRkpYcKXIXia9abywQ= @@ -637,6 +649,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -648,16 +662,13 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -684,8 +695,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -703,19 +714,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -731,8 +738,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -761,25 +768,22 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -789,8 +793,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -826,8 +830,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -907,11 +911,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -920,7 +921,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= diff --git a/internal/cli/commands_install_to.go b/internal/cli/commands_install_to.go new file mode 100644 index 000000000..3c506559a --- /dev/null +++ b/internal/cli/commands_install_to.go @@ -0,0 +1,81 @@ +package cli + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func installToCmd(destDir string) error { + // Get the path to the current executable + executablePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // Get the base name of the executable + executableName := filepath.Base(executablePath) + + // Create destination path + destPath := filepath.Join(destDir, executableName) + tempPath := destPath + ".tmp" + + // Remove temp file on error + defer func() { + if err != nil { + if _, statErr := os.Stat(tempPath); statErr == nil { + os.Remove(tempPath) + } + } + }() + + // Get source file info to preserve permissions + sourceInfo, err := os.Stat(executablePath) + if err != nil { + return fmt.Errorf("failed to get source file info: %w", err) + } + + // Open source file + sourceFile, err := os.Open(executablePath) + if err != nil { + return fmt.Errorf("failed to open source file: %w", err) + } + defer sourceFile.Close() + + // Create temp destination file + tempFile, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + defer tempFile.Close() + + // Copy the file content + _, err = io.Copy(tempFile, sourceFile) + if err != nil { + return fmt.Errorf("failed to copy file content: %w", err) + } + + // Ensure all writes are flushed + err = tempFile.Sync() + if err != nil { + return fmt.Errorf("failed to sync temp file: %w", err) + } + + // Close the temp file before renaming + tempFile.Close() + + // Set the correct permissions (preserve executable bit) + err = os.Chmod(tempPath, sourceInfo.Mode()) + if err != nil { + return fmt.Errorf("failed to set permissions: %w", err) + } + + // Atomically move temp file to final destination + err = os.Rename(tempPath, destPath) + if err != nil { + return fmt.Errorf("failed to move temp file to destination: %w", err) + } + + return nil +} diff --git a/internal/cli/commands_install_to_test.go b/internal/cli/commands_install_to_test.go new file mode 100644 index 000000000..f6df75f04 --- /dev/null +++ b/internal/cli/commands_install_to_test.go @@ -0,0 +1,339 @@ +package cli + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestInstallToCmdSuccess(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + destDir := filepath.Join(tempDir, "success") + err := os.MkdirAll(destDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Run the install command + err = installToCmd(destDir) + if err != nil { + t.Fatalf("installToCmd failed: %v", err) + } + + // Get the current executable name + executablePath, err := os.Executable() + if err != nil { + t.Fatalf("Failed to get executable path: %v", err) + } + executableName := filepath.Base(executablePath) + + // Check that the file was created + installedPath := filepath.Join(destDir, executableName) + info, err := os.Stat(installedPath) + if err != nil { + t.Fatalf("Installed binary not found: %v", err) + } + + // Check that it's executable (has execute permission) + if info.Mode().Perm()&0111 == 0 { + t.Error("Installed binary is not executable") + } + + // Check that the file size matches the original + originalInfo, err := os.Stat(executablePath) + if err != nil { + t.Fatalf("Failed to stat original executable: %v", err) + } + + if info.Size() != originalInfo.Size() { + t.Errorf("Installed binary size (%d) doesn't match original (%d)", info.Size(), originalInfo.Size()) + } + + // Check that permissions are preserved + if info.Mode().Perm() != originalInfo.Mode().Perm() { + t.Errorf("Permissions not preserved: got %v, want %v", info.Mode().Perm(), originalInfo.Mode().Perm()) + } +} + +func TestInstallToCmdNonexistentDirectory(t *testing.T) { + tempDir := t.TempDir() + nonexistentDir := filepath.Join(tempDir, "nonexistent", "deep", "path") + + err := installToCmd(nonexistentDir) + if err == nil { + t.Error("Expected error for nonexistent directory, got nil") + } + + // Should contain a meaningful error message + if err != nil && err.Error() == "" { + t.Error("Error message should not be empty") + } +} + +func TestInstallToCmdDestinationIsFile(t *testing.T) { + tempDir := t.TempDir() + // Create a file instead of directory + filePath := filepath.Join(tempDir, "notadir") + file, err := os.Create(filePath) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + file.Close() + + err = installToCmd(filePath) + if err == nil { + t.Error("Expected error when destination is a file, got nil") + } +} + +func TestInstallToCmdOverwriteExisting(t *testing.T) { + tempDir := t.TempDir() + destDir := filepath.Join(tempDir, "overwrite") + err := os.MkdirAll(destDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Get the current executable name + executablePath, err := os.Executable() + if err != nil { + t.Fatalf("Failed to get executable path: %v", err) + } + executableName := filepath.Base(executablePath) + existingPath := filepath.Join(destDir, executableName) + + // Create a dummy file to overwrite + dummyContent := []byte("dummy content") + err = os.WriteFile(existingPath, dummyContent, 0644) + if err != nil { + t.Fatalf("Failed to create dummy file: %v", err) + } + + // Run the install command + err = installToCmd(destDir) + if err != nil { + t.Fatalf("installToCmd failed: %v", err) + } + + // Check that the file was overwritten + installedContent, err := os.ReadFile(existingPath) + if err != nil { + t.Fatalf("Failed to read installed binary: %v", err) + } + + // Should not be the dummy content anymore + if string(installedContent) == string(dummyContent) { + t.Error("File was not overwritten - still contains dummy content") + } + + // Check that it's executable + info, err := os.Stat(existingPath) + if err != nil { + t.Fatalf("Failed to stat installed binary: %v", err) + } + + if info.Mode().Perm()&0111 == 0 { + t.Error("Overwritten binary is not executable") + } +} + +func TestInstallToCmdTempFileCleanup(t *testing.T) { + tempDir := t.TempDir() + destDir := filepath.Join(tempDir, "cleanup") + err := os.MkdirAll(destDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Get the current executable name + executablePath, err := os.Executable() + if err != nil { + t.Fatalf("Failed to get executable path: %v", err) + } + executableName := filepath.Base(executablePath) + + // We can't easily simulate a failure in the middle of installToCmd, + // but we can check that no temp files are left after successful execution + err = installToCmd(destDir) + if err != nil { + t.Fatalf("installToCmd failed: %v", err) + } + + // Check that no .tmp files are left behind + tempPattern := filepath.Join(destDir, executableName+".tmp") + matches, err := filepath.Glob(tempPattern) + if err != nil { + t.Fatalf("Failed to glob for temp files: %v", err) + } + + if len(matches) > 0 { + t.Errorf("Temp files left behind: %v", matches) + } + + // Also check for any .tmp files in the directory + entries, err := os.ReadDir(destDir) + if err != nil { + t.Fatalf("Failed to read directory: %v", err) + } + + for _, entry := range entries { + if filepath.Ext(entry.Name()) == ".tmp" { + t.Errorf("Temp file left behind: %s", entry.Name()) + } + } +} + +func TestInstallToCmdPreservesOriginal(t *testing.T) { + tempDir := t.TempDir() + destDir := filepath.Join(tempDir, "preserve") + err := os.MkdirAll(destDir, 0755) + if err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Get original file info before copy + executablePath, err := os.Executable() + if err != nil { + t.Fatalf("Failed to get executable path: %v", err) + } + + originalInfo, err := os.Stat(executablePath) + if err != nil { + t.Fatalf("Failed to stat original file: %v", err) + } + + // Run the install command + err = installToCmd(destDir) + if err != nil { + t.Fatalf("installToCmd failed: %v", err) + } + + // Check that original file is unchanged + afterInfo, err := os.Stat(executablePath) + if err != nil { + t.Fatalf("Failed to stat original file after copy: %v", err) + } + + if originalInfo.Size() != afterInfo.Size() { + t.Error("Original file size changed during copy") + } + + if originalInfo.ModTime() != afterInfo.ModTime() { + t.Error("Original file modification time changed during copy") + } + + if originalInfo.Mode() != afterInfo.Mode() { + t.Error("Original file mode changed during copy") + } +} + +// TestInstallToCmdIntegration tests the command through the CLI interface +func TestInstallToCmdIntegration(t *testing.T) { + // Build a test binary first + tempDir := t.TempDir() + testBinary := filepath.Join(tempDir, "migrate-test") + + // Find the repo root by looking for go.mod + repoRoot := "." + for i := 0; i < 5; i++ { // Look up to 5 levels up + if _, err := os.Stat(filepath.Join(repoRoot, "go.mod")); err == nil { + break + } + repoRoot = filepath.Join("..", repoRoot) + } + + cmd := exec.Command("go", "build", "-o", testBinary, "./cmd/migrate") + cmd.Dir = repoRoot + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Failed to build test binary: %v, output: %s", err, output) + } + + t.Run("successful installation via CLI", func(t *testing.T) { + destDir := filepath.Join(tempDir, "cli-success") + err := os.MkdirAll(destDir, 0755) + if err != nil { + t.Fatalf("Failed to create destination directory: %v", err) + } + + cmd := exec.Command(testBinary, "install-to", destDir) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Command failed: %v, output: %s", err, output) + } + + // Check that the binary was installed + installedPath := filepath.Join(destDir, "migrate-test") + if _, err := os.Stat(installedPath); err != nil { + t.Fatalf("Installed binary not found: %v", err) + } + + // Check that output contains success message + outputStr := string(output) + if !strings.Contains(outputStr, "Binary successfully installed") { + t.Errorf("Expected success message in output, got: %s", outputStr) + } + }) + + t.Run("error when no directory specified", func(t *testing.T) { + cmd := exec.Command(testBinary, "install-to") + output, err := cmd.CombinedOutput() + if err == nil { + t.Error("Expected command to fail when no directory specified") + } + + // Check that it exits with non-zero code + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 0 { + t.Error("Expected non-zero exit code") + } + } + + // Check error message + outputStr := string(output) + if !strings.Contains(outputStr, "please specify destination directory") { + t.Errorf("Expected error message in output, got: %s", outputStr) + } + }) + + t.Run("error when directory doesn't exist", func(t *testing.T) { + nonexistentDir := filepath.Join(tempDir, "nonexistent") + cmd := exec.Command(testBinary, "install-to", nonexistentDir) + output, err := cmd.CombinedOutput() + if err == nil { + t.Error("Expected command to fail for nonexistent directory") + } + + // Check that it exits with non-zero code + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 0 { + t.Error("Expected non-zero exit code") + } + } + + // Check error message + outputStr := string(output) + if !strings.Contains(outputStr, "destination directory does not exist") { + t.Errorf("Expected error message in output, got: %s", outputStr) + } + }) + + t.Run("help message", func(t *testing.T) { + cmd := exec.Command(testBinary, "install-to", "--help") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Help command failed: %v", err) + } + + outputStr := string(output) + if !strings.Contains(outputStr, "install-to DIR") { + t.Errorf("Expected install-to command in help output, got: %s", outputStr) + } + if !strings.Contains(outputStr, "Copy the running binary to the specified directory") { + t.Errorf("Expected install-to description in help output, got: %s", outputStr) + } + }) +} diff --git a/internal/cli/log.go b/internal/cli/log.go index b17754197..91c6474f2 100644 --- a/internal/cli/log.go +++ b/internal/cli/log.go @@ -2,7 +2,7 @@ package cli import ( "fmt" - logpkg "log" + logpkg "github.com/sirupsen/logrus" "os" ) diff --git a/internal/cli/main.go b/internal/cli/main.go index c7a3bd74a..d5e2cb9b2 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -1,8 +1,9 @@ package cli import ( - "flag" + "database/sql" "fmt" + "net/url" "os" "os/signal" "strconv" @@ -10,8 +11,12 @@ import ( "syscall" "time" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database" + "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source" ) @@ -24,13 +29,16 @@ const ( Use -format option to specify a Go time format string. Note: migrations with the same time cause "duplicate migration version" error. Use -tz option to specify the timezone that will be used when generating non-sequential migrations (defaults: UTC). ` - gotoUsage = `goto V Migrate to version V` + gotoUsage = `goto V [-force-dirty-handling] [-cache-dir P] Migrate to version V + Use -force-dirty-handling to handle dirty database state + Use -cache-dir to specify the intermediate path P for storing migrations` upUsage = `up [N] Apply all or N up migrations` downUsage = `down [N] [-all] Apply all or N down migrations Use -all to apply all down migrations` dropUsage = `drop [-f] Drop everything inside database Use -f to bypass confirmation` - forceUsage = `force V Set version V but don't run migration (ignores dirty state)` + forceUsage = `force V Set version V but don't run migration (ignores dirty state)` + installToUsage = `install-to DIR Copy the running binary to the specified directory` ) func handleSubCmdHelp(help bool, usage string, flagSet *flag.FlagSet) { @@ -58,16 +66,30 @@ func printUsageAndExit() { os.Exit(2) } +func dbMakeConnectionString(driver, user, password, address, name, ssl string) string { + return fmt.Sprintf("%s://%s:%s@%s/%s?sslmode=%s", + driver, url.QueryEscape(user), url.QueryEscape(password), address, name, ssl, + ) +} + // Main function of a cli application. It is public for backwards compatibility with `cli` package func Main(version string) { - helpPtr := flag.Bool("help", false, "") - versionPtr := flag.Bool("version", false, "") - verbosePtr := flag.Bool("verbose", false, "") - prefetchPtr := flag.Uint("prefetch", 10, "") - lockTimeoutPtr := flag.Uint("lock-timeout", 15, "") - pathPtr := flag.String("path", "", "") - databasePtr := flag.String("database", "", "") - sourcePtr := flag.String("source", "", "") + help := viper.GetBool("help") + version = viper.GetString("version") + verbose := viper.GetBool("verbose") + prefetch := viper.GetInt("prefetch") + lockTimeout := viper.GetInt("lock-timeout") + path := viper.GetString("path") + sourcePtr := viper.GetString("source") + + databasePtr := viper.GetString("database.dsn") + if databasePtr == "" { + databasePtr = dbMakeConnectionString( + viper.GetString("database.driver"), viper.GetString("database.user"), + viper.GetString("database.password"), viper.GetString("database.address"), + viper.GetString("database.name"), viper.GetString("database.ssl"), + ) + } flag.Usage = func() { fmt.Fprintf(os.Stderr, @@ -75,14 +97,25 @@ func Main(version string) { migrate [ -version | -help ] Options: - -source Location of the migrations (driver://url) - -path Shorthand for -source=file://path - -database Run migrations against this database (driver://url) - -prefetch N Number of migrations to load in advance before executing (default 10) - -lock-timeout N Allow N seconds to acquire database lock (default 15) - -verbose Print verbose logging - -version Print version - -help Print usage + --source Location of the migrations (driver://url) + --path Shorthand for -source=file://path + --database Run migrations against this database (driver://url) + --prefetch N Number of migrations to load in advance before executing (default 10) + --lock-timeout N Allow N seconds to acquire database lock (default 15) + --verbose Print verbose logging + --version Print version + --help Print usage + + // Infoblox specific + --config.source directory of the configuration file (default "/cli/config") + --config.file configuration file name (without extension) + --database.dsn database connection string + --database.driver database driver (default postgres) + --database.address address of the database (default "0.0.0.0:5432") + --database.name name of the database + --database.user database username (default "postgres") + --database.password database password (default "postgres") + --database.ssl database ssl mode (default "disable") Commands: %s @@ -91,38 +124,57 @@ Commands: %s %s %s + %s version Print current migration version Source drivers: `+strings.Join(source.List(), ", ")+` -Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoUsage, upUsage, downUsage, dropUsage, forceUsage) +Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoUsage, upUsage, downUsage, dropUsage, forceUsage, installToUsage) } - flag.Parse() - // initialize logger - log.verbose = *verbosePtr + log.verbose = verbose // show cli version - if *versionPtr { + if version == "" { fmt.Fprintln(os.Stderr, version) os.Exit(0) } // show help - if *helpPtr { + if help { flag.Usage() os.Exit(0) } // translate -path into -source if given - if *sourcePtr == "" && *pathPtr != "" { - *sourcePtr = fmt.Sprintf("file://%v", *pathPtr) + if sourcePtr == "" && path != "" { + sourcePtr = fmt.Sprintf("file://%v", path) } // initialize migrate // don't catch migraterErr here and let each command decide // how it wants to handle the error - migrater, migraterErr := migrate.New(*sourcePtr, *databasePtr) + var migrater *migrate.Migrate + var migraterErr error + + if driver := viper.GetString("database.driver"); driver == "hotload" { + db, err := sql.Open(driver, databasePtr) + if err != nil { + log.fatalErr(fmt.Errorf("could not open hotload dsn %s: %s", databasePtr, err)) + } + var dbname, user string + if err := db.QueryRow("SELECT current_database(), user").Scan(&dbname, &user); err != nil { + log.fatalErr(fmt.Errorf("could not get current_database: %s", err.Error())) + } + // dbname is not needed since it gets filled in by the driver but we want to be complete + migrateDriver, err := postgres.WithInstance(db, &postgres.Config{DatabaseName: dbname}) + if err != nil { + log.fatalErr(fmt.Errorf("could not create migrate driver: %s", err)) + } + migrater, migraterErr = migrate.NewWithDatabaseInstance(sourcePtr, dbname, migrateDriver) + } else { + migrater, migraterErr = migrate.New(sourcePtr, databasePtr) + } defer func() { if migraterErr == nil { if _, err := migrater.Close(); err != nil { @@ -132,8 +184,8 @@ Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoU }() if migraterErr == nil { migrater.Log = log - migrater.PrefetchMigrations = *prefetchPtr - migrater.LockTimeout = time.Duration(int64(*lockTimeoutPtr)) * time.Second + migrater.PrefetchMigrations = uint(prefetch) + migrater.LockTimeout = time.Duration(int64(lockTimeout)) * time.Second // handle Ctrl+c signals := make(chan os.Signal, 1) @@ -214,8 +266,19 @@ Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoU if err != nil { log.fatal("error: can't read version argument V") } + handleDirty := viper.GetBool("force-dirty-handling") + if handleDirty { + destPath := viper.GetString("cache-dir") + if destPath == "" { + log.fatal("error: cache-dir must be specified when force-dirty-handling is set") + } + + if err = migrater.WithDirtyStateConfig(sourcePtr, destPath, handleDirty); err != nil { + log.fatalErr(err) + } + } - if err := gotoCmd(migrater, uint(v)); err != nil { + if err = gotoCmd(migrater, uint(v)); err != nil { log.fatalErr(err) } @@ -362,6 +425,39 @@ Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoU log.Println("Finished after", time.Since(startTime)) } + case "install-to": + installToFlagSet, helpPtr := newFlagSetWithHelp("install-to") + + if err := installToFlagSet.Parse(args); err != nil { + log.fatalErr(err) + } + + handleSubCmdHelp(*helpPtr, installToUsage, installToFlagSet) + + if installToFlagSet.NArg() == 0 { + log.fatal("error: please specify destination directory") + } + + destDir := installToFlagSet.Arg(0) + + // Check if destination directory exists + if info, err := os.Stat(destDir); err != nil { + if os.IsNotExist(err) { + log.fatal("error: destination directory does not exist") + } + log.fatalErr(fmt.Errorf("error checking destination directory: %w", err)) + } else if !info.IsDir() { + log.fatal("error: destination path is not a directory") + } + + if err := installToCmd(destDir); err != nil { + log.fatalErr(err) + } + + if log.verbose { + log.Println("Binary successfully installed to", destDir) + } + case "version": if migraterErr != nil { log.fatalErr(migraterErr) diff --git a/migrate.go b/migrate.go index 266cc04eb..af1dd0ed2 100644 --- a/migrate.go +++ b/migrate.go @@ -7,7 +7,11 @@ package migrate import ( "errors" "fmt" + "net/url" "os" + "path/filepath" + "strconv" + "strings" "sync" "time" @@ -36,6 +40,9 @@ var ( ErrLockTimeout = errors.New("timeout: can't acquire database lock") ) +// Define a constant for the migration file name +const lastSuccessfulMigrationFile = "lastSuccessfulMigration" + // ErrShortLimit is an error returned when not enough migrations // can be returned by a source for a given limit. type ErrShortLimit struct { @@ -80,6 +87,21 @@ type Migrate struct { // LockTimeout defaults to DefaultLockTimeout, // but can be set per Migrate instance. LockTimeout time.Duration + + // dirtyStateConfig is used to store the configuration required to handle dirty state of the database + dirtyStateConf *dirtyStateConfig +} + +type dirtyStateConfig struct { + srcScheme string + srcPath string + destScheme string + destPath string + enable bool +} + +func (m *Migrate) IsDirtyHandlingEnabled() bool { + return m.dirtyStateConf != nil && m.dirtyStateConf.enable && m.dirtyStateConf.destPath != "" } // New returns a new Migrate instance from a source URL and a database URL. @@ -114,6 +136,20 @@ func New(sourceURL, databaseURL string) (*Migrate, error) { return m, nil } +func (m *Migrate) updateSourceDrv(sourceURL string) error { + sourceName, err := iurl.SchemeFromURL(sourceURL) + if err != nil { + return fmt.Errorf("failed to parse scheme from source URL: %w", err) + } + m.sourceName = sourceName + sourceDrv, err := source.Open(sourceURL) + if err != nil { + return fmt.Errorf("failed to open source, %q: %w", sourceURL, err) + } + m.sourceDrv = sourceDrv + return nil +} + // NewWithDatabaseInstance returns a new Migrate instance from a source URL // and an existing database instance. The source URL scheme is defined by each driver. // Use any string that can serve as an identifier during logging as databaseName. @@ -182,6 +218,39 @@ func NewWithInstance(sourceName string, sourceInstance source.Driver, databaseNa return m, nil } +func (m *Migrate) WithDirtyStateConfig(srcPath, destPath string, isDirty bool) error { + parsePath := func(path string) (string, string, error) { + uri, err := url.Parse(path) + if err != nil { + return "", "", err + } + scheme := "file" + if uri.Scheme != "file" && uri.Scheme != "" { + return "", "", fmt.Errorf("unsupported scheme: %s", scheme) + } + return scheme + "://", uri.Path, nil + } + + sScheme, sPath, err := parsePath(srcPath) + if err != nil { + return err + } + + dScheme, dPath, err := parsePath(destPath) + if err != nil { + return err + } + + m.dirtyStateConf = &dirtyStateConfig{ + srcScheme: sScheme, + destScheme: dScheme, + srcPath: sPath, + destPath: dPath, + enable: isDirty, + } + return nil +} + func newCommon() *Migrate { return &Migrate{ GracefulStop: make(chan bool, 1), @@ -215,20 +284,47 @@ func (m *Migrate) Migrate(version uint) error { if err := m.lock(); err != nil { return err } - curVersion, dirty, err := m.databaseDrv.Version() if err != nil { return m.unlockErr(err) } + // Sync migration scripts to database if supported + if err := m.syncMigrationsToDatabase(version); err != nil { + return m.unlockErr(err) + } + + // if the dirty flag is passed to the 'goto' command, handle the dirty state if dirty { - return m.unlockErr(ErrDirty{curVersion}) + if m.IsDirtyHandlingEnabled() { + if err = m.handleDirtyState(); err != nil { + return m.unlockErr(err) + } + } else { + // default behavior + return m.unlockErr(ErrDirty{curVersion}) + } + } + + // Copy migrations to the destination directory, + // if state was dirty when Migrate was called, we should handle the dirty state first before copying the migrations + if err = m.copyFiles(); err != nil { + return m.unlockErr(err) } ret := make(chan interface{}, m.PrefetchMigrations) go m.read(curVersion, int(version), ret) - return m.unlockErr(m.runMigrations(ret)) + if err = m.runMigrations(ret); err != nil { + return m.unlockErr(err) + } + // Success: Clean up and confirm + // Files are cleaned up after the migration is successful + if err = m.cleanupFiles(version); err != nil { + return m.unlockErr(err) + } + // unlock the database + return m.unlock() } // Steps looks at the currently active migration version. @@ -723,6 +819,7 @@ func (m *Migrate) readDown(from int, limit int, ret chan<- interface{}) { // to stop execution because it might have received a stop signal on the // GracefulStop channel. func (m *Migrate) runMigrations(ret <-chan interface{}) error { + var lastCleanMigrationApplied int for r := range ret { if m.stop() { @@ -744,6 +841,15 @@ func (m *Migrate) runMigrations(ret <-chan interface{}) error { if migr.Body != nil { m.logVerbosePrintf("Read and execute %v\n", migr.LogString()) if err := m.databaseDrv.Run(migr.BufferedBody); err != nil { + if m.IsDirtyHandlingEnabled() { + // this condition is required if the first migration fails + if lastCleanMigrationApplied == 0 { + lastCleanMigrationApplied = migr.TargetVersion + } + if e := m.handleMigrationFailure(lastCleanMigrationApplied); e != nil { + return multierror.Append(err, e) + } + } return err } } @@ -752,7 +858,7 @@ func (m *Migrate) runMigrations(ret <-chan interface{}) error { if err := m.databaseDrv.SetVersion(migr.TargetVersion, false); err != nil { return err } - + lastCleanMigrationApplied = migr.TargetVersion endTime := time.Now() readTime := migr.FinishedReading.Sub(migr.StartedBuffering) runTime := endTime.Sub(migr.FinishedReading) @@ -979,3 +1085,169 @@ func (m *Migrate) logErr(err error) { m.Log.Printf("error: %v", err) } } + +func (m *Migrate) handleDirtyState() error { + // Check if database supports migration storage + storageDriver, supportsStorage := m.databaseDrv.(database.MigrationStorageDriver) + + if supportsStorage { + return m.handleDirtyStateWithDatabase(storageDriver) + } + + return m.handleDirtyStateWithFiles() +} + +// handleDirtyStateWithDatabase handles dirty state using database-stored migrations +func (m *Migrate) handleDirtyStateWithDatabase(storageDriver database.MigrationStorageDriver) error { + // When using database storage, we can read the last successful migration + // from the database itself. The migration scripts are already stored there. + + // For now, we'll implement a simple approach: just create a temporary source + // that reads from the database instead of files + dbSource := &DatabaseSource{ + storageDriver: storageDriver, + logger: m.Log, + } + + // Temporarily replace the source driver + originalSource := m.sourceDrv + m.sourceDrv = dbSource + + // Restore original source when done + defer func() { + m.sourceDrv = originalSource + }() + + m.logPrintf("Handling dirty state using database-stored migrations") + return nil +} + +// handleDirtyStateWithFiles handles dirty state using file-based approach +func (m *Migrate) handleDirtyStateWithFiles() error { + // Perform the following actions when the database state is dirty + /* + 1. Update the source driver to read the migrations from the destination path + 2. Read the last successful migration version from the file + 3. Set the last successful migration version in the schema_migrations table + 4. Delete the last successful migration file + */ + // the source driver should read the migrations from the destination path + // as the DB is dirty and last applied migrations to the database are not present in the source path + if err := m.updateSourceDrv(m.dirtyStateConf.destScheme + m.dirtyStateConf.destPath); err != nil { + return err + } + lastSuccessfulMigrationPath := filepath.Join(m.dirtyStateConf.destPath, lastSuccessfulMigrationFile) + lastVersionBytes, err := os.ReadFile(lastSuccessfulMigrationPath) + if err != nil { + return err + } + lastVersionStr := strings.TrimSpace(string(lastVersionBytes)) + lastVersion, err := strconv.ParseInt(lastVersionStr, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse last successful migration version: %w", err) + } + + // Set the last successful migration version in the schema_migrations table + if err = m.databaseDrv.SetVersion(int(lastVersion), false); err != nil { + return fmt.Errorf("failed to apply last successful migration: %w", err) + } + + m.logPrintf("Successfully set last successful migration version: %s on the DB", lastVersionStr) + + if err = os.Remove(lastSuccessfulMigrationPath); err != nil { + return err + } + + m.logPrintf("Successfully deleted file: %s", lastSuccessfulMigrationPath) + return nil +} + +func (m *Migrate) handleMigrationFailure(lastSuccessfulMigration int) error { + if !m.IsDirtyHandlingEnabled() { + return nil + } + lastSuccessfulMigrationPath := filepath.Join(m.dirtyStateConf.destPath, lastSuccessfulMigrationFile) + return os.WriteFile(lastSuccessfulMigrationPath, []byte(strconv.Itoa(lastSuccessfulMigration)), 0644) +} + +func (m *Migrate) cleanupFiles(targetVersion uint) error { + if !m.IsDirtyHandlingEnabled() { + return nil + } + + files, err := os.ReadDir(m.dirtyStateConf.destPath) + if err != nil { + // If the directory does not exist + return fmt.Errorf("failed to read directory %s: %w", m.dirtyStateConf.destPath, err) + } + + for _, file := range files { + fileName := file.Name() + migration, err := source.Parse(fileName) + if err != nil { + return err + } + // Delete file if version is greater than targetVersion + if migration.Version > targetVersion { + if err = os.Remove(filepath.Join(m.dirtyStateConf.destPath, fileName)); err != nil { + m.logErr(fmt.Errorf("failed to delete file %s: %v", fileName, err)) + continue + } + m.logPrintf("Migration file: %s removed during cleanup", fileName) + } + } + + return nil +} + +// copyFiles copies all files from source to destination volume. +func (m *Migrate) copyFiles() error { + // this is the case when the dirty handling is disabled + if !m.IsDirtyHandlingEnabled() { + return nil + } + + files, err := os.ReadDir(m.dirtyStateConf.srcPath) + if err != nil { + // If the directory does not exist + return fmt.Errorf("failed to read directory %s: %w", m.dirtyStateConf.srcPath, err) + } + m.logPrintf("Copying files from %s to %s", m.dirtyStateConf.srcPath, m.dirtyStateConf.destPath) + for _, file := range files { + fileName := file.Name() + if source.Regex.MatchString(fileName) { + fileContentBytes, err := os.ReadFile(filepath.Join(m.dirtyStateConf.srcPath, fileName)) + if err != nil { + return err + } + info, err := file.Info() + if err != nil { + return err + } + if err = os.WriteFile(filepath.Join(m.dirtyStateConf.destPath, fileName), fileContentBytes, info.Mode().Perm()); err != nil { + return err + } + } + } + + m.logPrintf("Successfully Copied files from %s to %s", m.dirtyStateConf.srcPath, m.dirtyStateConf.destPath) + return nil +} + +// syncMigrationsToDatabase syncs migration scripts to the database if the driver supports it +func (m *Migrate) syncMigrationsToDatabase(targetVersion uint) error { + // Check if the database driver supports migration storage + storageDriver, ok := m.databaseDrv.(database.MigrationStorageDriver) + if !ok { + // Driver doesn't support storage, skip silently + return nil + } + + // Sync migrations up to the target version + if err := storageDriver.SyncMigrations(m.sourceDrv, targetVersion); err != nil { + return fmt.Errorf("failed to sync migrations to database: %w", err) + } + + m.logVerbosePrintf("Successfully synced migrations up to version %d to database", targetVersion) + return nil +} diff --git a/migrate_test.go b/migrate_test.go index f2728179e..4b83b92a0 100644 --- a/migrate_test.go +++ b/migrate_test.go @@ -4,9 +4,12 @@ import ( "bytes" "database/sql" "errors" + "fmt" "io" "log" "os" + "path/filepath" + "strconv" "strings" "testing" @@ -1414,3 +1417,345 @@ func equalDbSeq(t *testing.T, i int, expected migrationSequence, got *dStub.Stub t.Fatalf("\nexpected sequence %v,\ngot %v, in %v", bs, got.MigrationSequence, i) } } + +func setupMigrateInstance(tempDir string) (*Migrate, *dStub.Stub) { + scheme := "stub://" + m, _ := New(scheme, scheme) + m.dirtyStateConf = &dirtyStateConfig{ + destScheme: scheme, + destPath: tempDir, + enable: true, + } + return m, m.databaseDrv.(*dStub.Stub) +} + +func TestHandleDirtyState(t *testing.T) { + tempDir := t.TempDir() + + m, dbDrv := setupMigrateInstance(tempDir) + m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations + + tests := []struct { + lastSuccessfulVersion int + currentVersion int + err error + setupFailure bool + }{ + {lastSuccessfulVersion: 1, currentVersion: 3, err: nil, setupFailure: false}, + {lastSuccessfulVersion: 4, currentVersion: 7, err: nil, setupFailure: false}, + {lastSuccessfulVersion: 3, currentVersion: 4, err: nil, setupFailure: false}, + {lastSuccessfulVersion: -3, currentVersion: 4, err: ErrInvalidVersion, setupFailure: false}, + {lastSuccessfulVersion: 4, currentVersion: 3, err: fmt.Errorf("open %s: no such file or directory", filepath.Join(tempDir, lastSuccessfulMigrationFile)), setupFailure: true}, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + var lastSuccessfulMigrationPath string + // setupFailure flag helps with testing scenario where the 'lastSuccessfulMigrationFile' doesn't exist + if !test.setupFailure { + lastSuccessfulMigrationPath = filepath.Join(tempDir, lastSuccessfulMigrationFile) + if err := os.WriteFile(lastSuccessfulMigrationPath, []byte(strconv.Itoa(test.lastSuccessfulVersion)), 0644); err != nil { + t.Fatal(err) + } + } + // Setting the DB version as dirty + if err := dbDrv.SetVersion(test.currentVersion, true); err != nil { + t.Fatal(err) + } + + // Quick check to see if set correctly + version, b, err := dbDrv.Version() + if err != nil { + t.Fatal(err) + } + if version != test.currentVersion { + t.Fatalf("expected version %d, got %d", test.currentVersion, version) + } + + if !b { + t.Fatalf("expected DB to be dirty, got false") + } + + // Handle dirty state + if err = m.handleDirtyState(); err != nil { + if strings.Contains(err.Error(), test.err.Error()) { + t.Logf("expected error %v, got %v", test.err, err) + if !test.setupFailure { + if err = os.Remove(lastSuccessfulMigrationPath); err != nil { + t.Fatal(err) + } + } + return + } else { + t.Fatal(err) + } + } + // Check 1: DB should no longer be dirty + if dbDrv.IsDirty { + t.Fatalf("expected dirty to be false, got true") + } + // Check 2: Current version should be the last successful version + if dbDrv.CurrentVersion != test.lastSuccessfulVersion { + t.Fatalf("expected version %d, got %d", test.lastSuccessfulVersion, dbDrv.CurrentVersion) + } + // Check 3: The lastSuccessfulMigration file shouldn't exist + if _, err = os.Stat(lastSuccessfulMigrationPath); !os.IsNotExist(err) { + t.Fatalf("expected file to be deleted, but it still exists") + } + }) + } +} + +func TestHandleMigrationFailure(t *testing.T) { + tempDir := t.TempDir() + + m, _ := setupMigrateInstance(tempDir) + + tests := []struct { + lastSuccessFulVersion int + }{ + {lastSuccessFulVersion: 3}, + {lastSuccessFulVersion: 4}, + {lastSuccessFulVersion: 5}, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if err := m.handleMigrationFailure(test.lastSuccessFulVersion); err != nil { + t.Fatal(err) + } + // Check 1: last successful Migration version should be stored in a file + lastSuccessfulMigrationPath := filepath.Join(tempDir, lastSuccessfulMigrationFile) + if _, err := os.Stat(lastSuccessfulMigrationPath); os.IsNotExist(err) { + t.Fatalf("expected file to be created, but it does not exist") + } + + // Check 2: Check if the content of last successful migration has the correct version + content, err := os.ReadFile(lastSuccessfulMigrationPath) + if err != nil { + t.Fatal(err) + } + + if string(content) != strconv.Itoa(test.lastSuccessFulVersion) { + t.Fatalf("expected %d, got %s", test.lastSuccessFulVersion, string(content)) + } + }) + } +} + +func TestCleanupFiles(t *testing.T) { + tempDir := t.TempDir() + + m, _ := setupMigrateInstance(tempDir) + m.sourceDrv.(*sStub.Stub).Migrations = sourceStubMigrations + + tests := []struct { + migrationFiles []string + targetVersion uint + remainingFiles []string + emptyDestPath bool + }{ + { + migrationFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql"}, + targetVersion: 2, + remainingFiles: []string{"1_name.up.sql", "2_name.up.sql"}, + }, + { + migrationFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql", "4_name.up.sql", "5_name.up.sql"}, + targetVersion: 3, + remainingFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql"}, + }, + { + migrationFiles: []string{}, + targetVersion: 1, + remainingFiles: []string{}, + emptyDestPath: true, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + for _, file := range test.migrationFiles { + if err := os.WriteFile(filepath.Join(tempDir, file), []byte(""), 0644); err != nil { + t.Fatal(err) + } + } + + if test.emptyDestPath { + m.dirtyStateConf.destPath = "" + } + + if err := m.cleanupFiles(test.targetVersion); err != nil { + t.Fatal(err) + } + + // check 1: only files upto the target version should exist + for _, file := range test.remainingFiles { + if _, err := os.Stat(filepath.Join(tempDir, file)); os.IsNotExist(err) { + t.Fatalf("expected file %s to exist, but it does not", file) + } + } + + // check 2: the files removed are as expected + deletedFiles := diff(test.migrationFiles, test.remainingFiles) + for _, deletedFile := range deletedFiles { + if _, err := os.Stat(filepath.Join(tempDir, deletedFile)); !os.IsNotExist(err) { + t.Fatalf("expected file %s to be deleted, but it still exists", deletedFile) + } + } + }) + } +} + +func TestCopyFiles(t *testing.T) { + srcDir := t.TempDir() + destDir := t.TempDir() + + m, _ := setupMigrateInstance(destDir) + m.dirtyStateConf.srcPath = srcDir + + tests := []struct { + migrationFiles []string + copiedFiles []string + emptyDestPath bool + }{ + { + migrationFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql"}, + copiedFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql"}, + }, + { + migrationFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql", "4_name.up.sql", "current.sql"}, + copiedFiles: []string{"1_name.up.sql", "2_name.up.sql", "3_name.up.sql", "4_name.up.sql"}, + }, + { + emptyDestPath: true, // copyFiles should not do anything + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + for _, file := range test.migrationFiles { + if err := os.WriteFile(filepath.Join(srcDir, file), []byte(""), 0644); err != nil { + t.Fatal(err) + } + } + if test.emptyDestPath { + m.dirtyStateConf.destPath = "" + } + + if err := m.copyFiles(); err != nil { + t.Fatal(err) + } + + for _, file := range test.copiedFiles { + if _, err := os.Stat(filepath.Join(destDir, file)); os.IsNotExist(err) { + t.Fatalf("expected file %s to be copied, but it does not exist", file) + } + } + }) + } +} + +func TestWithDirtyStateConfig(t *testing.T) { + tests := []struct { + name string + srcPath string + destPath string + isDirty bool + wantErr bool + wantConf *dirtyStateConfig + }{ + { + name: "Valid file paths", + srcPath: "file:///src/path", + destPath: "file:///dest/path", + isDirty: true, + wantErr: false, + wantConf: &dirtyStateConfig{ + srcScheme: "file://", + destScheme: "file://", + srcPath: "/src/path", + destPath: "/dest/path", + enable: true, + }, + }, + { + name: "Invalid source scheme", + srcPath: "s3:///src/path", + destPath: "file:///dest/path", + isDirty: true, + wantErr: true, + }, + { + name: "Invalid destination scheme", + srcPath: "file:///src/path", + destPath: "s3:///dest/path", + isDirty: true, + wantErr: true, + }, + { + name: "Empty source scheme", + srcPath: "/src/path", + destPath: "file:///dest/path", + isDirty: true, + wantErr: false, + wantConf: &dirtyStateConfig{ + srcScheme: "file://", + destScheme: "file://", + srcPath: "/src/path", + destPath: "/dest/path", + enable: true, + }, + }, + { + name: "Empty destination scheme", + srcPath: "file:///src/path", + destPath: "/dest/path", + isDirty: true, + wantErr: false, + wantConf: &dirtyStateConfig{ + srcScheme: "file://", + destScheme: "file://", + srcPath: "/src/path", + destPath: "/dest/path", + enable: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Migrate{} + err := m.WithDirtyStateConfig(tt.srcPath, tt.destPath, tt.isDirty) + if (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && m.dirtyStateConf == tt.wantConf { + t.Errorf("dirtyStateConf = %v, want %v", m.dirtyStateConf, tt.wantConf) + } + }) + } +} + +/* + diff returns an array containing the elements in Array A and not in B +*/ + +func diff(a, b []string) []string { + temp := map[string]int{} + for _, s := range a { + temp[s]++ + } + for _, s := range b { + temp[s]-- + } + + var result []string + for s, v := range temp { + if v != 0 { + result = append(result, s) + } + } + return result +}