From 9dda103c4f546ca3197fcf08ae4e934024030fd7 Mon Sep 17 00:00:00 2001 From: Mohsen Mirzakhani Date: Tue, 19 Sep 2023 22:25:49 +0200 Subject: [PATCH] Testing server (#14) * testing api server * golang client * fix create schema issue * testing server container * fix server and db address issue * log api server address * logger * redis db * build docker for linux and darwin arm/amd64 --- .github/workflows/publish_docker.yaml | 4 + .github/workflows/release.yml | 2 - Makefile | 2 +- clients/golang/client.go | 289 +++++++++++++++++ clients/golang/client_test.go | 136 ++++++++ clients/golang/go.mod | 8 + clients/golang/go.sum | 14 + clients/golang/options.go | 87 +++++ .../test_sql/fixtures/00_add_some_records.sql | 3 + .../golang/test_sql/schema/migrations.up.sql | 4 + cmd/api.go | 43 +++ {internal/cmd => cmd}/list.go | 3 +- {internal/cmd => cmd}/root.go | 1 + {internal/cmd => cmd}/selfupdate.go | 8 +- {internal/cmd => cmd}/start/pg.go | 8 +- {internal/cmd => cmd}/start/redis.go | 2 +- {internal/cmd => cmd}/start/start.go | 0 {internal/cmd => cmd}/stop.go | 18 +- cmd/testing/start.go | 49 +++ cmd/testing/stop.go | 1 + go.mod | 1 + go.sum | 2 + internal/apiserver/container.go | 53 ++++ internal/apiserver/server.go | 298 ++++++++++++++++++ internal/container/models.go | 2 + internal/database/database.go | 18 +- internal/database/postgres/config.go | 16 + internal/database/postgres/pg.go | 276 ++++++++++++++-- internal/database/redis/redis.go | 133 +++++++- internal/logger/logger.go | 106 +++++++ internal/utils/utils.go | 11 + internal/utils/utils_test.go | 15 + main.go | 9 +- vendor/github.com/google/uuid/CHANGELOG.md | 10 + vendor/github.com/google/uuid/CONTRIBUTING.md | 26 ++ vendor/github.com/google/uuid/CONTRIBUTORS | 9 + vendor/github.com/google/uuid/LICENSE | 27 ++ vendor/github.com/google/uuid/README.md | 21 ++ vendor/github.com/google/uuid/dce.go | 80 +++++ vendor/github.com/google/uuid/doc.go | 12 + vendor/github.com/google/uuid/hash.go | 53 ++++ vendor/github.com/google/uuid/marshal.go | 38 +++ vendor/github.com/google/uuid/node.go | 90 ++++++ vendor/github.com/google/uuid/node_js.go | 12 + vendor/github.com/google/uuid/node_net.go | 33 ++ vendor/github.com/google/uuid/null.go | 118 +++++++ vendor/github.com/google/uuid/sql.go | 59 ++++ vendor/github.com/google/uuid/time.go | 123 ++++++++ vendor/github.com/google/uuid/util.go | 43 +++ vendor/github.com/google/uuid/uuid.go | 296 +++++++++++++++++ vendor/github.com/google/uuid/version1.go | 44 +++ vendor/github.com/google/uuid/version4.go | 76 +++++ vendor/modules.txt | 3 + 53 files changed, 2734 insertions(+), 61 deletions(-) create mode 100644 clients/golang/client.go create mode 100644 clients/golang/client_test.go create mode 100644 clients/golang/go.mod create mode 100644 clients/golang/go.sum create mode 100644 clients/golang/options.go create mode 100644 clients/golang/test_sql/fixtures/00_add_some_records.sql create mode 100644 clients/golang/test_sql/schema/migrations.up.sql create mode 100644 cmd/api.go rename {internal/cmd => cmd}/list.go (86%) rename {internal/cmd => cmd}/root.go (94%) rename {internal/cmd => cmd}/selfupdate.go (85%) rename {internal/cmd => cmd}/start/pg.go (88%) rename {internal/cmd => cmd}/start/redis.go (95%) rename {internal/cmd => cmd}/start/start.go (100%) rename {internal/cmd => cmd}/stop.go (84%) create mode 100644 cmd/testing/start.go create mode 100644 cmd/testing/stop.go create mode 100644 internal/apiserver/container.go create mode 100644 internal/apiserver/server.go create mode 100644 internal/logger/logger.go create mode 100644 internal/utils/utils_test.go create mode 100644 vendor/github.com/google/uuid/CHANGELOG.md create mode 100644 vendor/github.com/google/uuid/CONTRIBUTING.md create mode 100644 vendor/github.com/google/uuid/CONTRIBUTORS create mode 100644 vendor/github.com/google/uuid/LICENSE create mode 100644 vendor/github.com/google/uuid/README.md create mode 100644 vendor/github.com/google/uuid/dce.go create mode 100644 vendor/github.com/google/uuid/doc.go create mode 100644 vendor/github.com/google/uuid/hash.go create mode 100644 vendor/github.com/google/uuid/marshal.go create mode 100644 vendor/github.com/google/uuid/node.go create mode 100644 vendor/github.com/google/uuid/node_js.go create mode 100644 vendor/github.com/google/uuid/node_net.go create mode 100644 vendor/github.com/google/uuid/null.go create mode 100644 vendor/github.com/google/uuid/sql.go create mode 100644 vendor/github.com/google/uuid/time.go create mode 100644 vendor/github.com/google/uuid/util.go create mode 100644 vendor/github.com/google/uuid/uuid.go create mode 100644 vendor/github.com/google/uuid/version1.go create mode 100644 vendor/github.com/google/uuid/version4.go diff --git a/.github/workflows/publish_docker.yaml b/.github/workflows/publish_docker.yaml index ad4e6f1..0b6b925 100644 --- a/.github/workflows/publish_docker.yaml +++ b/.github/workflows/publish_docker.yaml @@ -25,6 +25,9 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -34,4 +37,5 @@ jobs: context: . file: ./Dockerfile push: true + platforms: linux/amd64,linux/arm64,darwin/amd64,darwin/arm64 tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c16fafd..95a3dde 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,6 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - - uses: actions/setup-go@v3 with: go-version: '1.19' diff --git a/Makefile b/Makefile index 84dae11..40c2a98 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ build: ## Build service binary. .PHONY: build_docker build_docker: ## Build docker image. - docker build -t dbctl:$(VERSION) -t dbctl:latest . + docker build -t mirzakhani/dbctl:$(VERSION) -t mirzakhani/dbctl:latest . .PHONY: install install: ## build and install the dbctl diff --git a/clients/golang/client.go b/clients/golang/client.go new file mode 100644 index 0000000..99d4c45 --- /dev/null +++ b/clients/golang/client.go @@ -0,0 +1,289 @@ +package golang + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "testing" +) + +var ( + // ErrInvalidDatabaseType is returned when an invalid database type is passed + ErrInvalidDatabaseType = errors.New("invalid database type") +) + +// Database types +const ( + // DatabasePostgres is a postgres database + DatabasePostgres = "postgres" + // DatabaseRedis is a redis database + DatabaseRedis = "redis" +) + +// MustCreatePostgresDB create a postgres database and return connection string or fail the test +func MustCreatePostgresDB(t *testing.T, opts ...Option) string { + return MustCreateDB(t, DatabasePostgres, opts...) +} + +// MustCreateRedisDB create a redis database and return connection string or fail the test +func MustCreateRedisDB(t *testing.T, opts ...Option) string { + return MustCreateDB(t, DatabaseRedis, opts...) +} + +// MustCreateDB create a database and return connection string or fail the test +// it will also remove the database after the test is finished +func MustCreateDB(t *testing.T, dbType string, opts ...Option) string { + uri, err := CreateDB(dbType, opts...) + if err != nil { + t.Fatalf("failed to create %s database: %v", dbType, err) + } + + t.Cleanup(func() { + if err := RemoveDB(dbType, uri); err != nil { + t.Fatalf("failed to remove %s database: %v", dbType, err) + } + }) + + return uri +} + +// RemoveDB remove a database using connection string +func RemoveDB(dbType, uri string) error { + return httpDoRemoveDBRequest(&RemoveDBRequest{Type: dbType, URI: uri}, defaultConfig.getHostURL()) +} + +// CreateDB create a database and return connection string +// it up to the caller to remove the database by calling RemoveDB +func CreateDB(dbType string, opts ...Option) (string, error) { + if dbType != DatabaseRedis && dbType != DatabasePostgres { + return "", ErrInvalidDatabaseType + } + + var cfg = defaultConfig + for _, opt := range opts { + if err := opt(cfg); err != nil { + return "", err + } + } + + var migrationsPath, fixturesPath string + if cfg.migrations != "" { + s, err := filepath.Abs(cfg.migrations) + if err != nil { + return "", fmt.Errorf("get migraions absolute path failed, %w", err) + } + migrationsPath = s + } + + if cfg.fixtures != "" { + s, err := filepath.Abs(cfg.fixtures) + if err != nil { + return "", fmt.Errorf("get fixtures absolute path failed, %w", err) + } + fixturesPath = s + } + + req := &CreateDBRequest{ + Type: dbType, + Migrations: migrationsPath, + Fixtures: fixturesPath, + InstanceName: cfg.instanceDBName, + InstancePass: cfg.instancePass, + InstancePort: cfg.instancePort, + InstanceUser: cfg.instanceUser, + } + + res, err := httpDoCreateDBRequest(req, cfg.getHostURL()) + if err != nil { + log.Println("httpDoCreateDBRequest failed:", err) + return "", err + } + + return res.URI, nil +} + +// ErrorMessage is representing rest api error object +type ErrorMessage struct { + Error string `json:"error"` +} + +// CreateDBRequest is the request object for creating a database +type CreateDBRequest struct { + Type string `json:"type"` + Migrations string `json:"migrations"` + Fixtures string `json:"fixtures"` + + // postgres instance information + InstancePort uint32 `json:"instance_port"` + InstanceUser string `json:"instance_user"` + InstancePass string `json:"instance_pass"` + InstanceName string `json:"instance_name"` +} + +// CreateDBResponse is the response object for creating a database +type CreateDBResponse struct { + URI string `json:"uri"` +} + +// RemoveDBRequest is the request object for removing a database +type RemoveDBRequest struct { + Type string `json:"type"` + URI string `json:"uri"` +} + +// Request is eatheir CreateDBRequest or RemoveDBRequest +type Request interface { + CreateDBRequest | RemoveDBRequest +} + +// Response is eatheir CreateDBResponse or ErrorMessage +type Response interface { + CreateDBResponse | interface{} +} + +func httpDoCreateDBRequest(r *CreateDBRequest, baseURL string) (*CreateDBResponse, error) { + url := baseURL + "/create" + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + migrationFiles, err := getFilesList(r.Migrations) + if err != nil { + log.Println("getFilesList migraions failed:", err) + return nil, err + } + + fixtureFiles, err := getFilesList(r.Fixtures) + if err != nil { + log.Println("getFilesList fixtures failed:", err) + return nil, err + } + + kv := map[string]string{ + "type": r.Type, + "instance_port": fmt.Sprintf("%d", r.InstancePort), + "instance_user": r.InstanceUser, + "instance_pass": r.InstancePass, + "instance_name": r.InstanceName, + } + + for _, f := range migrationFiles { + if err := addFileToWriter(bodyWriter, "migrations", f); err != nil { + return nil, err + } + } + + for _, f := range fixtureFiles { + if err := addFileToWriter(bodyWriter, "fixtures", f); err != nil { + return nil, err + } + } + + for k, v := range kv { + if err := bodyWriter.WriteField(k, v); err != nil { + return nil, err + } + } + + contentType := bodyWriter.FormDataContentType() + if err := bodyWriter.Close(); err != nil { + return nil, err + } + + req, err := http.Post(url, contentType, bodyBuf) + if err != nil { + return nil, err + } + defer req.Body.Close() + + if err := checkForError(req); err != nil { + return nil, err + } + + var res CreateDBResponse + if err := json.NewDecoder(req.Body).Decode(&res); err != nil { + return nil, err + } + + return &res, nil +} + +func httpDoRemoveDBRequest(r *RemoveDBRequest, baseURL string) error { + data, err := json.Marshal(r) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodDelete, baseURL+"/remove", bytes.NewReader(data)) + if err != nil { + return err + } + + rawRes, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer rawRes.Body.Close() + + return checkForError(rawRes) +} + +func addFileToWriter(w *multipart.Writer, fieldname, filename string) error { + fileWriter, err := w.CreateFormFile(fieldname, filename) + if err != nil { + return err + } + + f, err := os.Open(filename) + if err != nil { + return err + } + + if _, err := io.Copy(fileWriter, f); err != nil { + return err + } + + return nil +} + +func checkForError(r *http.Response) error { + if r.StatusCode >= 400 { + var err ErrorMessage + if err := json.NewDecoder(r.Body).Decode(&err); err != nil { + return err + } + return errors.New(err.Error) + } + return nil +} + +// getFilesList returns a list of files in a directory +func getFilesList(dir string) ([]string, error) { + if dir == "" { + return nil, nil + } + + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + absPath, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + var out []string + for _, f := range files { + out = append(out, filepath.Join(absPath, f.Name())) + } + + return out, nil +} diff --git a/clients/golang/client_test.go b/clients/golang/client_test.go new file mode 100644 index 0000000..2644a69 --- /dev/null +++ b/clients/golang/client_test.go @@ -0,0 +1,136 @@ +package golang + +import ( + "database/sql" + + "testing" + // golang postgres driver + "github.com/gomodule/redigo/redis" + _ "github.com/gomodule/redigo/redis" + _ "github.com/lib/pq" +) + +func TestMustCreatePostgresDB(t *testing.T) { + uri := MustCreatePostgresDB(t, WithMigrations("./test_sql/schema")) + if uri == "" { + t.Fatal("url is empty") + } + + t.Log("uri:", uri) + + conn, err := sql.Open("postgres", uri) + if err != nil { + t.Fatal(err) + } + + if err := conn.Ping(); err != nil { + t.Fatal(err) + } + defer func() { + _ = conn.Close() + }() + + // do something with conn + res, err := conn.Exec("insert into foo (name) values ('test-must-create-db')") + if err != nil { + t.Fatal(err) + } + + if re, _ := res.RowsAffected(); re != 1 { + t.Fatal("expected 1 rows affected") + } + + var name string + if err := conn.QueryRow("select name from foo").Scan(&name); err != nil { + t.Fatal(err) + } + + if name != "test-must-create-db" { + t.Fatalf("expected name to be test-must-create-db, got %s", name) + } +} + +func TestPostgresDBWithFixtures(t *testing.T) { + uri := MustCreatePostgresDB(t, WithMigrations("./test_sql/schema"), WithFixtures("./test_sql/fixtures")) + if uri == "" { + t.Fatal("url is empty") + } + + t.Log("uri:", uri) + + conn, err := sql.Open("postgres", uri) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = conn.Close() + }() + + // select all from foo + rows, err := conn.Query("select name from foo") + if err != nil { + t.Fatal(err) + } + + var names []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + t.Fatal(err) + } + names = append(names, name) + } + + if len(names) != 3 { + t.Fatal("expected 3 rows") + } + + expected := []string{"foo", "bar", "baz"} + for _, name := range names { + if !contains(expected, name) { + t.Fatalf("expected name to be one of %v, got %s", expected, name) + } + } +} + +func TestRedis(t *testing.T) { + uri := MustCreateRedisDB(t) + if uri == "" { + t.Fatal("url is empty") + } + + t.Log("uri:", uri) + + // do something with conn + conn, err := redis.DialURL(uri) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = conn.Close() + }() + + // set key + if _, err := conn.Do("SET", "foo", "bar"); err != nil { + t.Fatal(err) + } + + // get key + res, err := redis.String(conn.Do("GET", "foo")) + if err != nil { + t.Fatal(err) + } + + if res != "bar" { + t.Fatalf("expected res to be bar, got %s", res) + } +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/clients/golang/go.mod b/clients/golang/go.mod new file mode 100644 index 0000000..119f3cb --- /dev/null +++ b/clients/golang/go.mod @@ -0,0 +1,8 @@ +module github.com/mirzakhany/dbctl/clients/golang + +go 1.21.0 + +require ( + github.com/gomodule/redigo v1.8.9 + github.com/lib/pq v1.10.9 +) diff --git a/clients/golang/go.sum b/clients/golang/go.sum new file mode 100644 index 0000000..13d3c5a --- /dev/null +++ b/clients/golang/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/clients/golang/options.go b/clients/golang/options.go new file mode 100644 index 0000000..c0257f3 --- /dev/null +++ b/clients/golang/options.go @@ -0,0 +1,87 @@ +package golang + +import ( + "fmt" + "net" +) + +// config is the client configuration. +type config struct { + migrations string + fixtures string + + // whether or not to use default migrations/fixtures loaded when dbctl started + withDefaultMigrations bool + + // postgres instance information + instancePort uint32 + instanceUser string + instancePass string + instanceDBName string + + // host and port of the host, where the dbctl testing server is running + hostAddress string + hostPort uint32 +} + +var defaultConfig = &config{ + instancePass: "postgres", + instancePort: 15432, + instanceUser: "postgres", + instanceDBName: "postgres", + + hostAddress: "localhost", + hostPort: 1988, +} + +// Option is a function that configures the client. +type Option func(*config) error + +// WithMigrations configures the client to use the given migrations. +func WithMigrations(migrations string) Option { + return func(cfg *config) error { + cfg.migrations = migrations + return nil + } +} + +// WithDefaultMigrations configures the client to use the default migrations. +func WithDefaultMigrations() Option { + return func(cfg *config) error { + cfg.withDefaultMigrations = true + return nil + } +} + +// WithFixtures configures the client to use the given fixtures. +func WithFixtures(fixtures string) Option { + return func(cfg *config) error { + cfg.fixtures = fixtures + return nil + } +} + +// WithInstance configures the client to use the given postgres instance. +func WithInstance(user, pass, address, dbname string, port uint32) Option { + return func(cfg *config) error { + cfg.instanceUser = user + cfg.instancePass = pass + cfg.instanceDBName = dbname + cfg.instancePort = port + return nil + } +} + +// WithHost configures the client to use the given host. +func WithHost(address string, port uint32) Option { + return func(cfg *config) error { + cfg.hostAddress = address + cfg.hostPort = port + return nil + } +} + +// getHostURL returns the host url. +func (c *config) getHostURL() string { + return "http://" + net.JoinHostPort(c.hostAddress, fmt.Sprintf("%d", c.hostPort)) +} diff --git a/clients/golang/test_sql/fixtures/00_add_some_records.sql b/clients/golang/test_sql/fixtures/00_add_some_records.sql new file mode 100644 index 0000000..809a1a0 --- /dev/null +++ b/clients/golang/test_sql/fixtures/00_add_some_records.sql @@ -0,0 +1,3 @@ +insert into foo (id, name) values (1, 'foo'); +insert into foo (id, name) values (2, 'bar'); +insert into foo (id, name) values (3, 'baz'); diff --git a/clients/golang/test_sql/schema/migrations.up.sql b/clients/golang/test_sql/schema/migrations.up.sql new file mode 100644 index 0000000..615465c --- /dev/null +++ b/clients/golang/test_sql/schema/migrations.up.sql @@ -0,0 +1,4 @@ +create table foo( + id int, + name varchar(20) +); diff --git a/cmd/api.go b/cmd/api.go new file mode 100644 index 0000000..664e847 --- /dev/null +++ b/cmd/api.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "time" + + "github.com/mirzakhany/dbctl/internal/apiserver" + "github.com/mirzakhany/dbctl/internal/utils" + "github.com/spf13/cobra" +) + +// GetTestingAPIServerCmd represents the testing api server command +func GetTestingAPIServerCmd() *cobra.Command { + c := &cobra.Command{ + Aliases: []string{"api-server"}, + Use: "api-server", + Short: "api server is a http testing server to manage databases", + RunE: runTestingAPIServer, + } + + c.Flags().StringP("port", "p", apiserver.DefaultPort, "testing server default port") + c.Flags().BoolP("testing", "t", false, "run in testing mode with containerized server") + return c +} + +func runTestingAPIServer(cmd *cobra.Command, args []string) error { + port, err := cmd.Flags().GetString("port") + if err != nil { + return fmt.Errorf("invalid port args, %w", err) + } + + testing, err := cmd.Flags().GetBool("testing") + if err != nil { + return fmt.Errorf("invalid testing args, %w", err) + } + + if testing { + return apiserver.RunAPIServerContainer(utils.ContextWithOsSignal(), port, 20*time.Second) + } + + server := apiserver.NewServer(port) + return server.Start(utils.ContextWithOsSignal()) +} diff --git a/internal/cmd/list.go b/cmd/list.go similarity index 86% rename from internal/cmd/list.go rename to cmd/list.go index a802c6d..3f14461 100644 --- a/internal/cmd/list.go +++ b/cmd/list.go @@ -4,7 +4,6 @@ import ( "os" "github.com/mirzakhany/dbctl/internal/container" - "github.com/mirzakhany/dbctl/internal/database" "github.com/mirzakhany/dbctl/internal/table" "github.com/mirzakhany/dbctl/internal/utils" "github.com/spf13/cobra" @@ -31,7 +30,7 @@ func runList(_ *cobra.Command, _ []string) error { t := table.New(os.Stdout) t.AddRow("ID", "Name", "Type") for _, c := range containers { - t.AddRow(c.ID[:12], c.Name, c.Labels[database.LabelType]) + t.AddRow(c.ID[:12], c.Name, c.Labels[container.LabelType]) } t.Print() diff --git a/internal/cmd/root.go b/cmd/root.go similarity index 94% rename from internal/cmd/root.go rename to cmd/root.go index 9a0573d..e7a3472 100644 --- a/internal/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ func GetRootCmd(version string) *cobra.Command { Short: "Your swish knife of testing databases", Long: `Dbctl is a command line tools, providing simple command to run and manage databases for tests proposes`, + TraverseChildren: true, } return cmd diff --git a/internal/cmd/selfupdate.go b/cmd/selfupdate.go similarity index 85% rename from internal/cmd/selfupdate.go rename to cmd/selfupdate.go index 0b98a37..88bf923 100644 --- a/internal/cmd/selfupdate.go +++ b/cmd/selfupdate.go @@ -7,11 +7,13 @@ import ( "os" "strings" + "github.com/mirzakhany/dbctl/internal/logger" "github.com/mirzakhany/dbctl/internal/selfupdate" "github.com/mirzakhany/dbctl/internal/utils" "github.com/spf13/cobra" ) +// GetSelfUpdateCmd return self-update command func GetSelfUpdateCmd(version string) *cobra.Command { return &cobra.Command{ Use: "self-update", @@ -59,7 +61,7 @@ func doSelfUpdate(version string) error { log.Print("Do you want to update to ", latest, "? (y/n): ") input, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil || (input != "y\n" && input != "n\n") { - log.Println("Invalid input") + logger.Error("Invalid input") return err } if input == "n\n" { @@ -67,9 +69,9 @@ func doSelfUpdate(version string) error { } if err := updater.Update(ctx); err != nil { - log.Println("Error occurred while updating binary:", err) + logger.Error("Error occurred while updating binary:", err) return err } - log.Println("Successfully updated to version", latest) + logger.Info("Successfully updated to version", latest) return nil } diff --git a/internal/cmd/start/pg.go b/cmd/start/pg.go similarity index 88% rename from internal/cmd/start/pg.go rename to cmd/start/pg.go index 3b2b6d2..cee1a64 100644 --- a/internal/cmd/start/pg.go +++ b/cmd/start/pg.go @@ -18,10 +18,10 @@ func GetPgCmd() *cobra.Command { RunE: runPostgres, } - cmd.Flags().Uint32P("port", "p", 15432, "postgres default port") - cmd.Flags().StringP("user", "u", "postgres", "Database username") - cmd.Flags().String("pass", "postgres", "Database password") - cmd.Flags().StringP("name", "n", "postgres", "Database name") + cmd.Flags().Uint32P("port", "p", pg.DefaultPort, "postgres default port") + cmd.Flags().StringP("user", "u", pg.DefaultUser, "Database username") + cmd.Flags().String("pass", pg.DefaultPass, "Database password") + cmd.Flags().StringP("name", "n", pg.DefaultName, "Database name") cmd.Flags().StringP("version", "v", "", "Database version, default 14.3.2") cmd.Flags().StringP("migrations", "m", "", "Path to migration files, will be applied if provided") cmd.Flags().StringP("fixtures", "f", "", "Path to fixture files, its can be a file or directory.files in directory will be sorted by name before applying.") diff --git a/internal/cmd/start/redis.go b/cmd/start/redis.go similarity index 95% rename from internal/cmd/start/redis.go rename to cmd/start/redis.go index ac31006..50db8c0 100644 --- a/internal/cmd/start/redis.go +++ b/cmd/start/redis.go @@ -18,7 +18,7 @@ func GetRedisCmd() *cobra.Command { RunE: runRedis, } - cmd.Flags().Uint32P("port", "p", 16379, "Redis default port") + cmd.Flags().Uint32P("port", "p", redis.DefaultPort, "Redis default port") cmd.Flags().Int("db", 0, "Redis db index") cmd.Flags().StringP("user", "u", "", "Database username") cmd.Flags().String("pass", "", "Database password") diff --git a/internal/cmd/start/start.go b/cmd/start/start.go similarity index 100% rename from internal/cmd/start/start.go rename to cmd/start/start.go diff --git a/internal/cmd/stop.go b/cmd/stop.go similarity index 84% rename from internal/cmd/stop.go rename to cmd/stop.go index 570d092..2842c68 100644 --- a/internal/cmd/stop.go +++ b/cmd/stop.go @@ -15,7 +15,7 @@ import ( // GetStopCmd represents the stop command func GetStopCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "stop {rs pg id}", + Use: "stop {rs pg id all}", Short: "stop one or more detached databases", Long: `using this command you can stop one or more detached databases by their type or id, for example: dbctl stop pg rs or dbctl stop 969ec9747052`, @@ -58,6 +58,22 @@ func runStop(_ *cobra.Command, args []string) error { // TODO check if database is in detached mode and warn user // TODO hot fixing a bug, need to be refactored if len(args) == 1 && args[0] != "pg" && args[0] != "rs" && args[0] != "postgres" && args[0] != "redis" { + // check if its all then remove all + if args[0] == "all" { + containers, err := container.List(ctx, nil) + if err != nil { + return err + } + + for _, c := range containers { + if err := container.TerminateByID(ctx, c.ID); err != nil { + return err + } + } + return nil + } + + // remove by id if err := container.TerminateByID(ctx, args[0]); err != nil { return err } diff --git a/cmd/testing/start.go b/cmd/testing/start.go new file mode 100644 index 0000000..0441bcf --- /dev/null +++ b/cmd/testing/start.go @@ -0,0 +1,49 @@ +package testing + +import ( + "github.com/spf13/cobra" +) + +// +// dbctl testing -- \ +// pg -p 5435 -m ./migrations -f ./fixtures - \ +// rs -p 7654 +// + +// GetStartTestingCmd represents the start testing command +func GetStartTestingCmd(rootCmd *cobra.Command) *cobra.Command { + var cmd = &cobra.Command{ + Use: "testing -- pg [options] - rs [options]", + Short: "Start dbctl server for unit testing", + Run: func(cobraCmd *cobra.Command, args []string) { + var cmdParts []string + var cmdList [][]string + for _, arg := range args { + if arg == "-" { + if len(cmdParts) > 0 { + cmdList = append(cmdList, cmdParts) + cmdParts = []string{} + } + } else { + cmdParts = append(cmdParts, arg) + } + } + cmdList = append(cmdList, cmdParts) + + // run db commands + for _, cmdParts := range cmdList { + m := []string{"start", "-d"} + m = append(m, cmdParts...) + rootCmd.SetArgs(m) + rootCmd.Execute() + } + + // run api server + rootCmd.SetArgs([]string{"api-server", "-t"}) + rootCmd.Execute() + }, + } + + cmd.Flags().SetInterspersed(false) + return cmd +} diff --git a/cmd/testing/stop.go b/cmd/testing/stop.go new file mode 100644 index 0000000..7603f83 --- /dev/null +++ b/cmd/testing/stop.go @@ -0,0 +1 @@ +package testing diff --git a/go.mod b/go.mod index ef53380..4a3deac 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/docker/go-connections v0.4.0 github.com/gomodule/redigo v1.8.9 + github.com/google/uuid v1.3.1 github.com/lib/pq v1.10.0 github.com/spf13/cobra v1.5.0 ) diff --git a/go.sum b/go.sum index d1aa2f6..dd5a18e 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= diff --git a/internal/apiserver/container.go b/internal/apiserver/container.go new file mode 100644 index 0000000..89668c0 --- /dev/null +++ b/internal/apiserver/container.go @@ -0,0 +1,53 @@ +package apiserver + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "net" + "time" + + "github.com/mirzakhany/dbctl/internal/container" + "github.com/mirzakhany/dbctl/internal/logger" +) + +const labelAPIServer = "apiserver" + +// RunAPIServerContainer runs a container with the apiserver image +func RunAPIServerContainer(ctx context.Context, port string, timeout time.Duration) error { + var rnd, err = rand.Int(rand.Reader, big.NewInt(20)) + if err != nil { + return err + } + + _, err = container.Run(ctx, container.CreateRequest{ + Image: "mirzakhani/dbctl:latest", + Env: map[string]string{ + "DBCTL_INSIDE_DOCKER": "true", + }, + Cmd: []string{"/dbctl", "api-server"}, + ExposedPorts: []string{fmt.Sprintf("%s:1988/tcp", port)}, + Name: fmt.Sprintf("dbctl_apiserver_%d_%d", time.Now().Unix(), rnd.Uint64()), + Labels: map[string]string{container.LabelType: labelAPIServer}, + }) + if err != nil { + return err + } + + // wait for the container port to be ready + for { + conn, err := net.DialTimeout("tcp", net.JoinHostPort("", port), timeout) + if err != nil { + if err == context.DeadlineExceeded { + return err + } + } else { + _ = conn.Close() + break + } + } + + logger.Info("Started apiserver on http://localhost:" + port) + return nil +} diff --git a/internal/apiserver/server.go b/internal/apiserver/server.go new file mode 100644 index 0000000..84e25b1 --- /dev/null +++ b/internal/apiserver/server.go @@ -0,0 +1,298 @@ +package apiserver + +import ( + "context" + "encoding/json" + "io" + "log" + "net" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/mirzakhany/dbctl/internal/database" + pg "github.com/mirzakhany/dbctl/internal/database/postgres" + rs "github.com/mirzakhany/dbctl/internal/database/redis" + "github.com/mirzakhany/dbctl/internal/logger" +) + +// DefaultPort is the default port for the testing server +const DefaultPort = "1988" + +// Server is the testing server +type Server struct { + port string +} + +// NewServer creates a new testing server +func NewServer(port string) *Server { + return &Server{port: port} +} + +// Start starts the testing server +func (s *Server) Start(ctx context.Context) error { + mux := http.NewServeMux() + + mux.Handle("/create", http.HandlerFunc(s.CreateDB)) + mux.Handle("/remove", http.HandlerFunc(s.RemoveDB)) + + srv := &http.Server{ + Addr: net.JoinHostPort("", s.port), + Handler: mux, + } + + errs := make(chan error, 1) + go func() { + logger.Info("starting testing server on port", s.port) + if err := srv.ListenAndServe(); err != nil { + errs <- err + } + }() + + select { + case <-ctx.Done(): + logger.Info("shutting down testing server") + // graceful shutdown + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Fatalf("testing server shutdown failed, %v", err) + } + + return nil + case err := <-errs: + return err + } +} + +// CreateDBRequest is the request body for creating a database +type CreateDBRequest struct { + Type string `json:"type"` + Migrations string `json:"migrations"` + Fixtures string `json:"fixtures"` + + // postgres instance information + InstanceHost string `json:"instance_host"` + InstancePort uint32 `json:"instance_port"` + InstanceUser string `json:"instance_user"` + InstancePass string `json:"instance_pass"` + InstanceName string `json:"instance_name"` +} + +// CreateDBResponse is the response body for creating a database +type CreateDBResponse struct { + URI string `json:"uri"` +} + +// CreateDB creates a new database +func (s *Server) CreateDB(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + JSONError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + req := &CreateDBRequest{ + Type: r.FormValue("type"), + InstancePass: r.FormValue("instance_pass"), + InstanceUser: r.FormValue("instance_user"), + InstanceName: r.FormValue("instance_name"), + InstanceHost: r.FormValue("instance_host"), + } + + if req.Type == "" { + JSONError(w, http.StatusBadRequest, "type is required") + return + } + + // check if type is one of valid options + if req.Type != "postgres" && req.Type != "redis" { + JSONError(w, http.StatusBadRequest, "type is not valid, valid options are postgres or redis") + return + } + + migrationsDir, err := os.MkdirTemp("/tmp", "migrations-*") + if err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + defer os.RemoveAll(migrationsDir) + + fixturesDir, err := os.MkdirTemp("/tmp", "fixtures-*") + if err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + defer os.RemoveAll(fixturesDir) + + // read migrations + if err := readMulipartFiles(r, "migrations", migrationsDir); err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + req.Migrations = migrationsDir + + // read fixtures + if err := readMulipartFiles(r, "fixtures", fixturesDir); err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + req.Fixtures = fixturesDir + + var uri string + var createErr error + + switch req.Type { + case "postgres": + uri, createErr = createPostgresDB(r.Context(), req) + case "redis": + uri, createErr = createRedisDB(r.Context(), req) + } + + if createErr != nil { + JSONError(w, http.StatusInternalServerError, createErr.Error()) + return + } + + JSON(w, http.StatusOK, CreateDBResponse{URI: uri}) +} + +func readMulipartFiles(r *http.Request, key, dst string) error { + for _, f := range r.MultipartForm.File[key] { + dst, err := os.Create(filepath.Join(dst, f.Filename)) + if err != nil { + return err + } + f, err := f.Open() + if err != nil { + return err + } + io.Copy(dst, f) + } + return nil +} + +// RemoveDBRequest is the request body for removing a database +type RemoveDBRequest struct { + Type string `json:"type"` + URI string `json:"uri"` +} + +// RemoveDB removes the given database +func (s *Server) RemoveDB(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + JSONError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + + req := &RemoveDBRequest{} + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + JSONError(w, http.StatusBadRequest, err.Error()) + return + } + + if req.Type == "" { + JSONError(w, http.StatusBadRequest, "type is required") + return + } + + if req.URI == "" { + JSONError(w, http.StatusBadRequest, "uri is required") + return + } + + var err error + switch req.Type { + case "postgres": + err = removePostgresDB(r.Context(), req) + case "redis": + err = removeRedisDB(r.Context(), req) + } + + if err != nil { + JSONError(w, http.StatusInternalServerError, err.Error()) + return + } + + JSON(w, http.StatusNoContent, `"{"message":"db removed successfully"}"`) +} + +func createPostgresDB(ctx context.Context, r *CreateDBRequest) (string, error) { + if r.InstancePort == 0 { + r.InstancePort = pg.DefaultPort + } + + if r.InstanceUser == "" { + r.InstanceUser = pg.DefaultUser + } + + if r.InstancePass == "" { + r.InstancePass = pg.DefaultPass + } + + if r.InstanceName == "" { + r.InstanceName = pg.DefaultName + } + + pgDB, _ := pg.New(pg.WithHost(r.InstanceUser, r.InstancePass, r.InstanceName, r.InstancePort)) + res, err := pgDB.CreateDB(ctx, &database.CreateDBRequest{ + Migrations: r.Migrations, + Fixtures: r.Fixtures, + }) + + if err != nil { + return "", err + } + + return res.URI, nil +} + +func createRedisDB(ctx context.Context, r *CreateDBRequest) (string, error) { + if r.InstancePort == 0 { + r.InstancePort = rs.DefaultPort + } + + if r.InstanceUser == "" { + r.InstanceUser = rs.DefaultUser + } + + if r.InstancePass == "" { + r.InstancePass = rs.DefaultPass + } + + // TODO handle redis fixtures + rsDB, _ := rs.New() + res, err := rsDB.CreateDB(ctx, &database.CreateDBRequest{}) + + if err != nil { + return "", err + } + + return res.URI, nil +} + +func removePostgresDB(ctx context.Context, r *RemoveDBRequest) error { + pgDB, _ := pg.New() + return pgDB.RemoveDB(ctx, r.URI) +} + +func removeRedisDB(ctx context.Context, r *RemoveDBRequest) error { + rsDB, _ := rs.New() + return rsDB.RemoveDB(ctx, r.URI) +} + +// JSONError writes the given status code and error message to the ResponseWriter. +func JSONError(w http.ResponseWriter, status int, err string) { + JSON(w, status, map[string]string{"error": err}) +} + +// JSON writes the given status code and data to the ResponseWriter. +func JSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(data); err != nil { + logger.Error("error encoding json", err) + return + } +} diff --git a/internal/container/models.go b/internal/container/models.go index f38f429..1d39861 100644 --- a/internal/container/models.go +++ b/internal/container/models.go @@ -2,6 +2,8 @@ package container import "github.com/docker/go-connections/nat" +const LabelType = "dbctl_type" + type Container struct { ID string Name string diff --git a/internal/database/database.go b/internal/database/database.go index 53fd56b..d44e8b5 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -15,10 +15,10 @@ const ( ) const ( - LabelType = "dbctl_type" LabelPostgres = "postgres" LabelPGWeb = "pgweb" LabelRedis = "redis" + LabelTesting = "testing" ) type Info struct { @@ -33,3 +33,19 @@ type Database interface { WaitForStart(ctx context.Context, timeout time.Duration) error URI() string } + +type CreateDBRequest struct { + Migrations string + Fixtures string + + WithDefaultMigraions bool +} + +type CreateDBResponse struct { + URI string +} + +type Admin interface { + CreateDB(ctx context.Context, req *CreateDBRequest) (*CreateDBResponse, error) + RemoveDB(ctx context.Context, uri string) error +} diff --git a/internal/database/postgres/config.go b/internal/database/postgres/config.go index d5537cc..a813cab 100644 --- a/internal/database/postgres/config.go +++ b/internal/database/postgres/config.go @@ -1,6 +1,7 @@ package pg import ( + "crypto/sha256" "fmt" "io" "os" @@ -35,8 +36,10 @@ var ( } ) +// Option is the type of the functional options for the postgres type Option func(*config) error +// WithUI applied withUI option to config func WithUI(withIU bool) Option { return func(c *config) error { c.withUI = withIU @@ -44,6 +47,7 @@ func WithUI(withIU bool) Option { } } +// WithHost applied selected postgres host to config func WithHost(user, pass, name string, port uint32) Option { return func(c *config) error { c.user = user @@ -81,6 +85,7 @@ func getVersions() []string { return out } +// WithLogger applied selected logger to config func WithLogger(logger io.Writer) Option { return func(c *config) error { c.logger = logger @@ -88,6 +93,7 @@ func WithLogger(logger io.Writer) Option { } } +// WithMigrations applied selected migrations to config func WithMigrations(path string) Option { return func(c *config) error { files, err := getFiles(path) @@ -107,6 +113,7 @@ func WithMigrations(path string) Option { } } +// WithFixtures applied selected fixtures to config func WithFixtures(path string) Option { return func(c *config) error { files, err := getFiles(path) @@ -162,3 +169,12 @@ func getPostGisImage(version string) string { // fallback to odidev/postgis:13-3.1 return "odidev/postgis:13-3.1-alpine" } + +// get hash generate a hash from list of strings +func getHash(list []string) string { + sort.Strings(list) + xx := fmt.Sprintf("%x", list) + // create md5 hash of xx + cc := sha256.Sum256([]byte(xx)) + return string(cc[:]) +} diff --git a/internal/database/postgres/pg.go b/internal/database/postgres/pg.go index fbe679f..21bf044 100644 --- a/internal/database/postgres/pg.go +++ b/internal/database/postgres/pg.go @@ -4,8 +4,8 @@ import ( "context" "crypto/rand" "database/sql" + "errors" "fmt" - "log" "math/big" "net" "net/url" @@ -14,26 +14,49 @@ import ( "strings" "time" + "github.com/mirzakhany/dbctl/internal/logger" + "github.com/mirzakhany/dbctl/internal/utils" + // golang postgres driver _ "github.com/lib/pq" "github.com/mirzakhany/dbctl/internal/container" "github.com/mirzakhany/dbctl/internal/database" ) -var _ database.Database = (*Postgres)(nil) +var ( + _ database.Database = (*Postgres)(nil) + _ database.Admin = (*Postgres)(nil) + + errDatabaseNotExists = errors.New("database does not exist") +) +const ( + // DefaultPort is the default port for postgres + DefaultPort = 15432 + // DefaultUser is the default user for postgres + DefaultUser = "postgres" + // DefaultPass is the default password for postgres + DefaultPass = "postgres" + // DefaultName is the default database name for postgres + DefaultName = "postgres" + // DefaultTemplate is the default template name for postgres when creating a new database with migtations and fixtures + DefaultTemplate = "dbctl_template" +) + +// Postgres is a postgres database instance type Postgres struct { containerID string cfg config } +// New creates a new postgres database instance controller func New(options ...Option) (*Postgres, error) { // create postgres with default values pg := &Postgres{cfg: config{ - pass: "postgres", - user: "postgres", - name: "postgres", - port: 15432, + pass: DefaultPass, + user: DefaultUser, + name: DefaultName, + port: DefaultPort, version: "14.3.0", }} @@ -42,30 +65,180 @@ func New(options ...Option) (*Postgres, error) { return nil, err } } + return pg, nil } +// CreateDB creates a new database with given migrations and fixtures +func (p *Postgres) CreateDB(ctx context.Context, req *database.CreateDBRequest) (*database.CreateDBResponse, error) { + // connect to default database + conn, err := dbConnect(ctx, p.URI()) + if err != nil { + return nil, err + } + defer func() { + _ = conn.Close() + }() + + // create a random name for new database + dbName := fmt.Sprintf("dbctl_%d", time.Now().UnixNano()) + newDB, _ := New(WithHost(p.cfg.user, p.cfg.pass, dbName, p.cfg.port)) + newURI := newDB.URI() + + if req.WithDefaultMigraions { + if err = p.createDatabaseWithTemplate(ctx, conn, dbName, DefaultTemplate); err != nil { + if errors.Is(err, errDatabaseNotExists) { + return nil, fmt.Errorf("default database not found, please create it first: %w", err) + } + } + + // run apply fixtures if exist + if len(req.Fixtures) != 0 { + if err := applyFixturesFromDir(ctx, conn, req.Fixtures, newURI); err != nil { + return nil, err + } + } + + //retun new database uri + return &database.CreateDBResponse{URI: newURI}, nil + } + + // if no migrations provided, just create a new database + if len(req.Migrations) == 0 { + logger.Debug("No migrations provided, creating a new database ...") + if err := createDatabase(ctx, conn, dbName); err != nil { + return nil, err + } + return &database.CreateDBResponse{URI: newURI}, nil + } + + logger.Debug("Creating a new database with migrations ...") + // if migrations provided, create a template database and create a new database from template + // new a new database with provided migrations and fixtures + // run migrations if exist + migrationFiles, err := getFiles(req.Migrations) + if err != nil { + return nil, fmt.Errorf("read migraions failed: %w", err) + } + templateName := utils.GetListHash(migrationFiles) + logger.Debug("template name is:", templateName) + + // try to create database using template + err = p.createDatabaseWithTemplate(ctx, conn, dbName, templateName) + if err != nil && !errors.Is(err, errDatabaseNotExists) { + logger.Debug("create database with template failed, trying to create a new database ...") + return nil, err + } + + if errors.Is(err, errDatabaseNotExists) { + logger.Debug("template database not found, creating a new database ...") + // create database if not exist + if err := createDatabase(ctx, conn, dbName); err != nil { + return nil, err + } + + logger.Debug("template database found, creating a new database from template ...") + // connect to new database and run migrations + if err := RunMigrations(ctx, nil, migrationFiles, newURI); err != nil { + return nil, err + } + + // create a template from new database + _ = p.createDatabaseWithTemplate(ctx, conn, templateName, dbName) + } + + if len(req.Fixtures) != 0 { + if err := applyFixturesFromDir(ctx, nil, req.Fixtures, newURI); err != nil { + return nil, err + } + } + + // make sure we retrun localhost instead of host.docker.internal + if os.Getenv("DBCTL_INSIDE_DOCKER") == "true" { + newURI = strings.ReplaceAll(newURI, "host.docker.internal", "localhost") + } + + return &database.CreateDBResponse{URI: newURI}, nil +} + +func (p *Postgres) createDatabaseWithTemplate(ctx context.Context, conn *sql.DB, name, template string) error { + if conn == nil { + var err error + conn, err = dbConnect(ctx, p.URI()) + if err != nil { + return err + } + defer func() { + _ = conn.Close() + }() + } + + // if default is exist, use it as template and create new database + if _, err := conn.Exec(fmt.Sprintf("create database %q with template %q", name, template)); err != nil { + // is error database not exist? + if strings.Contains(err.Error(), "does not exist") { + return errDatabaseNotExists + } + return fmt.Errorf("create database with template failed: %w", err) + } + return nil +} + +// RemoveDB removes a database from postgres by given uri +func (p *Postgres) RemoveDB(ctx context.Context, uri string) error { + // parse the uri to get database name + u, err := url.Parse(uri) + if err != nil { + return err + } + + // get database name + dbName := strings.TrimPrefix(u.Path, "/") + + conn, err := dbConnect(ctx, p.URI()) + if err != nil { + return err + } + defer func() { + _ = conn.Close() + }() + + // terminate connection + _, _ = conn.ExecContext(ctx, fmt.Sprintf("select pg_terminate_backend(pid) from pg_stat_activity where datname = %s", dbName)) + if _, err := conn.ExecContext(ctx, fmt.Sprintf("drop database if exists %s", dbName)); err != nil { + return fmt.Errorf("drop database failed: %v", err) + } + + return nil +} + +// Start starts a postgres database func (p *Postgres) Start(ctx context.Context, detach bool) error { - log.Printf("Starting postgres version %s on port %d ...\n", p.cfg.version, p.cfg.port) + logger.Info(fmt.Sprintf("Starting postgres version %s on port %d ...", p.cfg.version, p.cfg.port)) closeFunc, err := p.startUsingDocker(ctx, 20*time.Second) if err != nil { return err } - log.Println("Postgres is up and running") + logger.Info("Postgres is up and running") // run migrations if exist - if err := RunMigrations(ctx, p.cfg.migrationsFiles, p.URI()); err != nil { + if err := RunMigrations(ctx, nil, p.cfg.migrationsFiles, p.URI()); err != nil { return err } - // run apply fixtures if exist - if err := ApplyFixtures(ctx, p.cfg.fixtureFiles, p.URI()); err != nil { - return err + // create template database if migrations exist + if len(p.cfg.migrationsFiles) > 0 { + _ = p.createDatabaseWithTemplate(ctx, nil, DefaultTemplate, p.cfg.name) + + // run apply fixtures if exist + if err := ApplyFixtures(ctx, nil, p.cfg.fixtureFiles, p.URI()); err != nil { + return err + } } // print connection url - log.Printf("Database uri is: %q\n", p.URI()) + logger.Info(fmt.Sprintf("Database uri is: %q", p.URI())) var pgwebCloseFunc database.CloseFunc if p.cfg.withUI { @@ -82,7 +255,7 @@ func (p *Postgres) Start(ctx context.Context, detach bool) error { } <-ctx.Done() - log.Println("Shutdown signal received, stopping database") + logger.Info("Shutdown signal received, stopping database") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer func() { @@ -99,12 +272,14 @@ func (p *Postgres) Start(ctx context.Context, detach bool) error { return closeFunc(shutdownCtx) } +// Stop stops a postgres database func (p *Postgres) Stop(ctx context.Context) error { return container.TerminateByID(ctx, p.containerID) } +// WaitForStart waits for postgres to start func (p *Postgres) WaitForStart(ctx context.Context, timeout time.Duration) error { - log.Println("Wait for database to boot up") + logger.Info("Wait for database to boot up") ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() @@ -114,7 +289,7 @@ func (p *Postgres) WaitForStart(ctx context.Context, timeout time.Duration) erro for range ticker.C { conn, err := dbConnect(ctx, p.URI()) if err != nil { - if err == context.DeadlineExceeded { + if errors.Is(err, context.DeadlineExceeded) { return err } } else { @@ -126,7 +301,7 @@ func (p *Postgres) WaitForStart(ctx context.Context, timeout time.Duration) erro } func (p *Postgres) runUI(ctx context.Context) (database.CloseFunc, error) { - log.Println("Starting postgres ui using pgweb (https://github.com/sosedoff/pgweb)") + logger.Info("Starting postgres ui using pgweb (https://github.com/sosedoff/pgweb)") var rnd, err = rand.Int(rand.Reader, big.NewInt(20)) if err != nil { @@ -141,14 +316,14 @@ func (p *Postgres) runUI(ctx context.Context) (database.CloseFunc, error) { }, ExposedPorts: []string{"8081:8081"}, Name: fmt.Sprintf("dbctl_pgweb_%d_%d", time.Now().Unix(), rnd.Uint64()), - Labels: map[string]string{database.LabelType: database.LabelPGWeb}, + Labels: map[string]string{container.LabelType: database.LabelPGWeb}, }) if err != nil { return nil, err } // log ui url - log.Println("Database UI is running on: http://localhost:8081") + logger.Info("Database UI is running on: http://localhost:8081") closeFunc := func(ctx context.Context) error { return pgweb.Terminate(ctx) @@ -157,8 +332,9 @@ func (p *Postgres) runUI(ctx context.Context) (database.CloseFunc, error) { return closeFunc, nil } +// Instances returns a list of postgres instances func Instances(ctx context.Context) ([]database.Info, error) { - l, err := container.List(ctx, map[string]string{database.LabelType: database.LabelPostgres}) + l, err := container.List(ctx, map[string]string{container.LabelType: database.LabelPostgres}) if err != nil { return nil, err } @@ -191,7 +367,7 @@ func (p *Postgres) startUsingDocker(ctx context.Context, timeout time.Duration) Cmd: []string{"postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"}, ExposedPorts: []string{fmt.Sprintf("%s:5432/tcp", port)}, Name: fmt.Sprintf("dbctl_pg_%d_%d", time.Now().Unix(), rnd.Uint64()), - Labels: map[string]string{database.LabelType: database.LabelPostgres}, + Labels: map[string]string{container.LabelType: database.LabelPostgres}, }) if err != nil { return nil, err @@ -206,37 +382,69 @@ func (p *Postgres) startUsingDocker(ctx context.Context, timeout time.Duration) return closeFunc, p.WaitForStart(ctx, timeout) } +// URI returns the postgres connection uri func (p *Postgres) URI() string { - host := net.JoinHostPort("localhost", strconv.Itoa(int(p.cfg.port))) + addr := "localhost" + if os.Getenv("DBCTL_INSIDE_DOCKER") == "true" { + addr = "host.docker.internal" + } + + host := net.JoinHostPort(addr, strconv.Itoa(int(p.cfg.port))) return (&url.URL{Scheme: "postgres", User: url.UserPassword(p.cfg.user, p.cfg.pass), Host: host, Path: p.cfg.name, RawQuery: "sslmode=disable"}).String() } -func RunMigrations(ctx context.Context, migrationsFiles []string, uri string) error { +// RunMigrations runs migrations on a postgres database +func RunMigrations(ctx context.Context, conn *sql.DB, migrationsFiles []string, uri string) error { if migrationsFiles == nil { return nil } - log.Println("Applying migrations ...") - return applySQL(ctx, migrationsFiles, uri) + logger.Info("Applying migrations ...") + return applySQL(ctx, conn, migrationsFiles, uri) } -func ApplyFixtures(ctx context.Context, fixtureFiles []string, uri string) error { +// ApplyFixtures applies fixtures on a postgres database +func ApplyFixtures(ctx context.Context, conn *sql.DB, fixtureFiles []string, uri string) error { if len(fixtureFiles) == 0 { return nil } - log.Println("Applying fixtures ...") - return applySQL(ctx, fixtureFiles, uri) + logger.Info("Applying fixtures ...") + return applySQL(ctx, conn, fixtureFiles, uri) } -func applySQL(ctx context.Context, stmts []string, uri string) error { - conn, err := dbConnect(ctx, uri) +func applyFixturesFromDir(ctx context.Context, conn *sql.DB, dir string, uri string) error { + if dir == "" { + return nil + } + + files, err := getFiles(dir) if err != nil { - return fmt.Errorf("unable to connect to database: %w", err) + return fmt.Errorf("read fixtures failed: %w", err) + } + + logger.Info("Applying fixtures ...") + return applySQL(ctx, conn, files, uri) +} + +func createDatabase(ctx context.Context, conn *sql.DB, name string) error { + if _, err := conn.ExecContext(ctx, fmt.Sprintf("create database %s", name)); err != nil { + return fmt.Errorf("create database failed: %w", err) + } + return nil +} + +func applySQL(ctx context.Context, conn *sql.DB, stmts []string, uri string) error { + if conn == nil { + var err error + conn, err = dbConnect(ctx, uri) + if err != nil { + return err + } + defer func() { + _ = conn.Close() + }() } - defer func() { - _ = conn.Close() - }() for _, f := range stmts { b, err := os.ReadFile(f) diff --git a/internal/database/redis/redis.go b/internal/database/redis/redis.go index edd7bef..6a1b53f 100644 --- a/internal/database/redis/redis.go +++ b/internal/database/redis/redis.go @@ -8,27 +8,44 @@ import ( "math/big" "net" "net/url" + "os" "strconv" + "strings" "time" "github.com/gomodule/redigo/redis" "github.com/mirzakhany/dbctl/internal/container" "github.com/mirzakhany/dbctl/internal/database" + "github.com/mirzakhany/dbctl/internal/logger" ) -var _ database.Database = (*Redis)(nil) +var ( + _ database.Database = (*Redis)(nil) + _ database.Admin = (*Redis)(nil) +) + +const ( + // DefaultPort is the default port for redis + DefaultPort = 16379 + // DefaultUser is the default user for redis + DefaultUser = "" + // DefaultPass is the default password for redis + DefaultPass = "" +) +// Redis is a redis database type Redis struct { containerID string cfg config } +// New creates a new redis database instance func New(options ...Option) (*Redis, error) { // create redis with default values rs := &Redis{cfg: config{ - pass: "", - user: "", - port: 16379, + pass: DefaultPass, + user: DefaultUser, + port: DefaultPort, version: "7.0.4", }} @@ -37,9 +54,91 @@ func New(options ...Option) (*Redis, error) { return nil, err } } + return rs, nil } +// CreateDB creates a new database +func (p *Redis) CreateDB(ctx context.Context, req *database.CreateDBRequest) (*database.CreateDBResponse, error) { + // get first available db index + dbIndex, err := p.getAvailableDBIndex(ctx) + if err != nil { + return nil, err + } + + p.cfg.dbIndex = dbIndex + uri := p.URI() + // make sure we retrun localhost instead of host.docker.internal + if os.Getenv("DBCTL_INSIDE_DOCKER") == "true" { + uri = strings.ReplaceAll(uri, "host.docker.internal", "localhost") + } + + return &database.CreateDBResponse{URI: uri}, nil +} + +func (p *Redis) getAvailableDBIndex(ctx context.Context) (int, error) { + // get or saw db index + conn, err := redis.DialURLContext(ctx, p.noAuthURI()) + if err != nil { + return 0, err + } + + defer func() { + _ = conn.Close() + }() + + stmt := redis.NewScript(0, ` + local dbIndex = redis.call("GET", "dbctl:dbIndex") + if not dbIndex then + dbIndex = 1 + end + redis.call("SET", "dbctl:dbIndex", dbIndex+1) + redis.call("SELECT", dbIndex) + redis.call("FLUSHDB") + return dbIndex + `) + + return redis.Int(stmt.Do(conn)) +} + +// RemoveDB removes a database by its uri +func (p *Redis) RemoveDB(ctx context.Context, uri string) error { + u, err := url.Parse(uri) + if err != nil { + return err + } + + // get db index from uri + dbIndex, err := strconv.Atoi(strings.TrimPrefix(u.Path, "/")) + if err != nil { + return err + } + + // remove db index and flush db + stmt := redis.NewScript(0, ` + local dbIndex = tonumber(ARGV[1]) + if dbIndex == 0 then + return + end + redis.call("SELECT", dbIndex) + redis.call("FLUSHDB") + redis.call("SET", "dbctl:dbIndex", dbIndex-1) + `) + + conn, err := redis.DialURLContext(ctx, p.noAuthURI()) + if err != nil { + return err + } + + defer func() { + _ = conn.Close() + }() + + _, err = stmt.Do(conn, dbIndex) + return err +} + +// Start starts the database func (p *Redis) Start(ctx context.Context, detach bool) error { log.Printf("Starting redis version %s on port %d ...\n", p.cfg.version, p.cfg.port) @@ -59,7 +158,7 @@ func (p *Redis) Start(ctx context.Context, detach bool) error { } <-ctx.Done() - log.Println("Shutdown signal received, stopping database") + logger.Info("Shutdown signal received, stopping database") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer func() { @@ -69,12 +168,14 @@ func (p *Redis) Start(ctx context.Context, detach bool) error { return closeFunc(shutdownCtx) } +// Stop stops the database func (p *Redis) Stop(ctx context.Context) error { return container.TerminateByID(ctx, p.containerID) } +// WaitForStart waits for database to boot up func (p *Redis) WaitForStart(ctx context.Context, timeout time.Duration) error { - log.Println("Wait for database to boot up") + logger.Info("Wait for database to boot up") ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() @@ -95,8 +196,9 @@ func (p *Redis) WaitForStart(ctx context.Context, timeout time.Duration) error { return nil } +// Instances returns a list of running redis instances func Instances(ctx context.Context) ([]database.Info, error) { - l, err := container.List(ctx, map[string]string{database.LabelType: database.LabelRedis}) + l, err := container.List(ctx, map[string]string{container.LabelType: database.LabelRedis}) if err != nil { return nil, err } @@ -128,7 +230,7 @@ func (p *Redis) startUsingDocker(ctx context.Context, timeout time.Duration) (fu }, ExposedPorts: []string{fmt.Sprintf("%s:6379/tcp", port)}, Name: fmt.Sprintf("dbctl_rs_%d_%d", time.Now().Unix(), rnd.Uint64()), - Labels: map[string]string{database.LabelType: database.LabelRedis}, + Labels: map[string]string{container.LabelType: database.LabelRedis}, }) if err != nil { return nil, err @@ -146,15 +248,26 @@ func (p *Redis) startUsingDocker(ctx context.Context, timeout time.Duration) (fu } func (p *Redis) noAuthURI() string { + addr := "localhost" + if os.Getenv("DBCTL_INSIDE_DOCKER") == "true" { + addr = "host.docker.internal" + } + return (&url.URL{ Scheme: "redis", - Host: net.JoinHostPort("localhost", strconv.Itoa(int(p.cfg.port))), + Host: net.JoinHostPort(addr, strconv.Itoa(int(p.cfg.port))), Path: strconv.Itoa(p.cfg.dbIndex), }).String() } +// URI returns the connection string for the database func (p *Redis) URI() string { - host := net.JoinHostPort("localhost", strconv.Itoa(int(p.cfg.port))) + addr := "localhost" + if os.Getenv("DBCTL_INSIDE_DOCKER") == "true" { + addr = "host.docker.internal" + } + + host := net.JoinHostPort(addr, strconv.Itoa(int(p.cfg.port))) var userInfo *url.Userinfo if p.cfg.user != "" && p.cfg.pass != "" { diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..e3a4809 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,106 @@ +package logger + +import "log" + +// Provider is the interface that wraps the Println method. +type Provider interface { + Println(v ...any) +} + +// LogLevel is the log level type. +type LogLevel int + +const ( + // LevelDebug is the debug log level + LevelDebug LogLevel = iota + // LevelInfo is the info log level + LevelInfo + // LevelWarn is the warn log level + LevelWarn + // LevelError is the error log level + LevelError +) + +func (l LogLevel) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "" + } +} + +// Logger is the logger object +type Logger struct { + provider Provider + + logLvl LogLevel +} + +// New creates a new logger with the given provider. +func New(provider Provider, loglvl LogLevel) *Logger { + return &Logger{provider: provider, logLvl: loglvl} +} + +// SetProvider sets the logger provider. +func SetProvider(provider Provider) { + logger.provider = provider +} + +// SetLevel sets the logger level. +func SetLevel(loglvl LogLevel) { + if loglvl < LevelDebug || loglvl > LevelError { + return + } + logger.logLvl = loglvl +} + +// logger is the default logger. +var logger = New(log.Default(), LevelDebug) + +// Println calls the Println method of the logger provider. +func Println(v ...any) { + logger.provider.Println(v...) +} + +// Info calls the Println method of the logger provider with INFO prefix. +func Info(v ...any) { + if logger.logLvl > LevelInfo { + return + } + printWithLevel(LevelInfo, v...) +} + +// Debug calls the Println method of the logger provider with DEBUG prefix. +func Debug(v ...any) { + if logger.logLvl > LevelDebug { + return + } + printWithLevel(LevelDebug, v...) +} + +// Warn calls the Println method of the logger provider with WARN prefix. +func Warn(v ...any) { + if logger.logLvl > LevelWarn { + return + } + printWithLevel(LevelWarn, v...) +} + +// Error calls the Println method of the logger provider with ERROR prefix. +func Error(v ...any) { + if logger.logLvl > LevelError { + return + } + printWithLevel(LevelError, v...) +} + +func printWithLevel(lvl LogLevel, v ...any) { + logger.provider.Println(append([]any{lvl.String() + ":"}, v...)...) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 63f896c..db6763d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -2,8 +2,11 @@ package utils import ( "context" + "crypto/sha256" + "fmt" "os" "os/signal" + "sort" "syscall" ) @@ -34,3 +37,11 @@ func Contain(src []string, target, alias string) bool { } return false } + +// GetListHash generate a hash from list of strings +func GetListHash(list []string) string { + sort.Strings(list) + // create md5 hash of xx + cc := sha256.Sum256([]byte(fmt.Sprintf("%x", list))) + return fmt.Sprintf("%x", cc[:]) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..000c198 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,15 @@ +package utils + +import "testing" + +func TestGetListHash(t *testing.T) { + list := []string{"a", "b", "c"} + hash := GetListHash(list) + if hash == "" { + t.Fatal("hash is empty") + } + + if hash != "dcd229f9224c1d8a1b514239d207f5be800d6a78001e5f550263db0fd05ff979" { + t.Fatalf("expected dcd229f9224c1d8a1b514239d207f5be800d6a78001e5f550263db0fd05ff979, got %s", hash) + } +} diff --git a/main.go b/main.go index 35d94a2..aa9ab36 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,9 @@ import ( "fmt" "os" - "github.com/mirzakhany/dbctl/internal/cmd" - "github.com/mirzakhany/dbctl/internal/cmd/start" + "github.com/mirzakhany/dbctl/cmd" + "github.com/mirzakhany/dbctl/cmd/start" + "github.com/mirzakhany/dbctl/cmd/testing" ) // version will be populated by the build script with the sha of the last git commit. @@ -22,6 +23,10 @@ func main() { root.AddCommand(cmd.GetStopCmd()) root.AddCommand(cmd.GetListCmd()) root.AddCommand(cmd.GetSelfUpdateCmd(version)) + root.AddCommand(cmd.GetTestingAPIServerCmd()) + + // testing is able to run multiple commands includes starting the dbctl api server + root.AddCommand(testing.GetStartTestingCmd(root)) if err := root.Execute(); err != nil { os.Exit(1) diff --git a/vendor/github.com/google/uuid/CHANGELOG.md b/vendor/github.com/google/uuid/CHANGELOG.md new file mode 100644 index 0000000..2bd7866 --- /dev/null +++ b/vendor/github.com/google/uuid/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18) + + +### Bug Fixes + +* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0)) + +## Changelog diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 0000000..5566888 --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Tips + +Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org). + +Always try to include a test case! If it is not possible or not necessary, +please explain why in the pull request description. + +### Releasing + +Commits that would precipitate a SemVer change, as desrcibed in the Conventional +Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action) +to create a release candidate pull request. Once submitted, `release-please` +will create a release. + +For tips on how to work with `release-please`, see its documentation. + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 0000000..b4bb97f --- /dev/null +++ b/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 0000000..5dc6826 --- /dev/null +++ b/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md new file mode 100644 index 0000000..3e9a618 --- /dev/null +++ b/vendor/github.com/google/uuid/README.md @@ -0,0 +1,21 @@ +# uuid +The uuid package generates and inspects UUIDs based on +[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +```sh +go get github.com/google/uuid +``` + +###### Documentation +[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://pkg.go.dev/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go new file mode 100644 index 0000000..fa820b9 --- /dev/null +++ b/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go new file mode 100644 index 0000000..5b8a4b9 --- /dev/null +++ b/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go new file mode 100644 index 0000000..b404f4b --- /dev/null +++ b/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 0000000..14bd340 --- /dev/null +++ b/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + return err + } + *uuid = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go new file mode 100644 index 0000000..d651a2b --- /dev/null +++ b/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 0000000..b2a0bc8 --- /dev/null +++ b/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This removes the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 0000000..0cbbcdd --- /dev/null +++ b/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/vendor/github.com/google/uuid/null.go b/vendor/github.com/google/uuid/null.go new file mode 100644 index 0000000..d7fcbf2 --- /dev/null +++ b/vendor/github.com/google/uuid/null.go @@ -0,0 +1,118 @@ +// Copyright 2021 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +var jsonNull = []byte("null") + +// NullUUID represents a UUID that may be null. +// NullUUID implements the SQL driver.Scanner interface so +// it can be used as a scan destination: +// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } +// +type NullUUID struct { + UUID UUID + Valid bool // Valid is true if UUID is not NULL +} + +// Scan implements the SQL driver.Scanner interface. +func (nu *NullUUID) Scan(value interface{}) error { + if value == nil { + nu.UUID, nu.Valid = Nil, false + return nil + } + + err := nu.UUID.Scan(value) + if err != nil { + nu.Valid = false + return err + } + + nu.Valid = true + return nil +} + +// Value implements the driver Valuer interface. +func (nu NullUUID) Value() (driver.Value, error) { + if !nu.Valid { + return nil, nil + } + // Delegate to UUID Value function + return nu.UUID.Value() +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (nu NullUUID) MarshalBinary() ([]byte, error) { + if nu.Valid { + return nu.UUID[:], nil + } + + return []byte(nil), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (nu *NullUUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(nu.UUID[:], data) + nu.Valid = true + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (nu NullUUID) MarshalText() ([]byte, error) { + if nu.Valid { + return nu.UUID.MarshalText() + } + + return jsonNull, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (nu *NullUUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + nu.Valid = false + return err + } + nu.UUID = id + nu.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +func (nu NullUUID) MarshalJSON() ([]byte, error) { + if nu.Valid { + return json.Marshal(nu.UUID) + } + + return jsonNull, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (nu *NullUUID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, jsonNull) { + *nu = NullUUID{} + return nil // valid null UUID + } + err := json.Unmarshal(data, &nu.UUID) + nu.Valid = err == nil + return err +} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go new file mode 100644 index 0000000..2e02ec0 --- /dev/null +++ b/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently. +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go new file mode 100644 index 0000000..e6ef06c --- /dev/null +++ b/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go new file mode 100644 index 0000000..5ea6c73 --- /dev/null +++ b/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 0000000..a56138c --- /dev/null +++ b/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,296 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +const randPoolSize = 16 * 16 + +var ( + rander = rand.Reader // random function + poolEnabled = false + poolMu sync.Mutex + poolPos = randPoolSize // protected with poolMu + pool [randPoolSize]byte // protected with poolMu +) + +type invalidLengthError struct{ len int } + +func (err invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + +// IsInvalidLengthError is matcher function for custom error invalidLengthError +func IsInvalidLengthError(err error) bool { + _, ok := err.(invalidLengthError) + return ok +} + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(s)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(b)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} + +// EnableRandPool enables internal randomness pool used for Random +// (Version 4) UUID generation. The pool contains random bytes read from +// the random number generator on demand in batches. Enabling the pool +// may improve the UUID generation throughput significantly. +// +// Since the pool is stored on the Go heap, this feature may be a bad fit +// for security sensitive applications. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func EnableRandPool() { + poolEnabled = true +} + +// DisableRandPool disables the randomness pool if it was previously +// enabled with EnableRandPool. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func DisableRandPool() { + poolEnabled = false + defer poolMu.Unlock() + poolMu.Lock() + poolPos = randPoolSize +} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go new file mode 100644 index 0000000..4631096 --- /dev/null +++ b/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go new file mode 100644 index 0000000..7697802 --- /dev/null +++ b/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,76 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewString creates a new random UUID and returns it as a string or panics. +// NewString is equivalent to the expression +// +// uuid.New().String() +func NewString() string { + return Must(NewRandom()).String() +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// Uses the randomness pool if it was enabled with EnableRandPool. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + if !poolEnabled { + return NewRandomFromReader(rander) + } + return newRandomFromPool() +} + +// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func NewRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +func newRandomFromPool() (UUID, error) { + var uuid UUID + poolMu.Lock() + if poolPos == randPoolSize { + _, err := io.ReadFull(rander, pool[:]) + if err != nil { + poolMu.Unlock() + return Nil, err + } + poolPos = 0 + } + copy(uuid[:], pool[poolPos:(poolPos+16)]) + poolPos += 16 + poolMu.Unlock() + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 17fea8a..392b396 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,6 +4,9 @@ github.com/docker/go-connections/nat # github.com/gomodule/redigo v1.8.9 ## explicit; go 1.16 github.com/gomodule/redigo/redis +# github.com/google/uuid v1.3.1 +## explicit +github.com/google/uuid # github.com/inconshreveable/mousetrap v1.0.1 ## explicit; go 1.18 github.com/inconshreveable/mousetrap