├── go.mod ├── pkg ├── models │ ├── template_database.go │ ├── test_database.go │ ├── database.go │ ├── database_config.go │ └── database_config_test.go └── util │ ├── env.go │ ├── testing.go │ ├── hash_test.go │ └── hash.go ├── .dockerignore ├── go.sum ├── .vscode ├── extensions.json ├── tasks.json └── launch.json ├── client_config.go ├── .gitignore ├── tools.go ├── docker-helper.sh ├── cmd └── cli │ └── main.go ├── LICENSE ├── Makefile ├── Dockerfile ├── docker-compose.yml ├── .devcontainer └── devcontainer.json ├── README.md ├── client.go └── client_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/allaboutapps/integresql-client-go 2 | 3 | go 1.14 4 | 5 | require github.com/lib/pq v1.3.0 6 | -------------------------------------------------------------------------------- /pkg/models/template_database.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type TemplateDatabase struct { 4 | Database `json:"database"` 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | .devcontainer 3 | .vscode 4 | .pkg 5 | .tools-versions 6 | Dockerfile 7 | docker-compose.* 8 | docker-helper.sh -------------------------------------------------------------------------------- /pkg/models/test_database.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type TestDatabase struct { 4 | Database `json:"database"` 5 | 6 | ID int `json:"id"` 7 | } 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 2 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 3 | -------------------------------------------------------------------------------- /pkg/models/database.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Database struct { 4 | TemplateHash string `json:"templateHash"` 5 | Config DatabaseConfig `json:"config"` 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "ms-azuretools.vscode-docker", 7 | "ms-vscode-remote.remote-containers" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "make", 8 | "type": "shell", 9 | "command": "make", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /client_config.go: -------------------------------------------------------------------------------- 1 | package integresql 2 | 3 | import "github.com/allaboutapps/integresql-client-go/pkg/util" 4 | 5 | type ClientConfig struct { 6 | BaseURL string 7 | APIVersion string 8 | } 9 | 10 | func DefaultClientConfigFromEnv() ClientConfig { 11 | return ClientConfig{ 12 | BaseURL: util.GetEnv("INTEGRESQL_CLIENT_BASE_URL", "http://integresql:5000/api"), 13 | APIVersion: util.GetEnv("INTEGRESQL_CLIENT_API_VERSION", "v1"), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # based on https://github.com/github/gitignore/blob/master/Go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | # GOBIN 20 | bin 21 | 22 | # local go mod cache 23 | .pkg 24 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | // Tooling dependencies 4 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 5 | // https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md 6 | 7 | // This file may incorporate tools that may be *both* used as CLI and as lib 8 | // Keep in mind that these global tools change the go.mod/go.sum dependency tree 9 | // Other tooling may be installed as *static binary* directly within the Dockerfile 10 | 11 | package tools 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}/cmd/server", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /docker-helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" = "--up" ]; then 4 | docker-compose up --no-start 5 | docker-compose start # ensure we are started, handle also allowed to be consumed by vscode 6 | docker-compose exec integresql-client bash 7 | fi 8 | 9 | if [ "$1" = "--halt" ]; then 10 | docker-compose stop 11 | fi 12 | 13 | if [ "$1" = "--destroy" ]; then 14 | docker-compose down --rmi local -v --remove-orphans 15 | fi 16 | 17 | [ -n "$1" -a \( "$1" = "--up" -o "$1" = "--halt" -o "$1" = "--destroy" \) ] \ 18 | || { echo "usage: $0 --up | --halt | --destroy" >&2; exit 1; } -------------------------------------------------------------------------------- /pkg/util/env.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | func GetEnv(key string, defaultVal string) string { 9 | if val, ok := os.LookupEnv(key); ok { 10 | return val 11 | } 12 | 13 | return defaultVal 14 | } 15 | 16 | func GetEnvAsInt(key string, defaultVal int) int { 17 | strVal := GetEnv(key, "") 18 | 19 | if val, err := strconv.Atoi(strVal); err == nil { 20 | return val 21 | } 22 | 23 | return defaultVal 24 | } 25 | 26 | func GetEnvAsBool(key string, defaultVal bool) bool { 27 | strVal := GetEnv(key, "") 28 | 29 | if val, err := strconv.ParseBool(strVal); err == nil { 30 | return val 31 | } 32 | 33 | return defaultVal 34 | } 35 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/allaboutapps/integresql-client-go" 12 | ) 13 | 14 | func main() { 15 | hash := os.Getenv("INTEGRESQL_CLIENT_TEMPLATE_HASH") 16 | if len(hash) == 0 { 17 | log.Fatalln("No template hash provided, please set INTEGRESQL_CLIENT_TEMPLATE_HASH") 18 | } 19 | 20 | c, err := integresql.DefaultClientFromEnv() 21 | if err != nil { 22 | log.Fatalf("Failed to create IntegreSQL client: %v", err) 23 | } 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 26 | defer cancel() 27 | 28 | test, err := c.GetTestDatabase(ctx, "meepmeep") 29 | if err != nil { 30 | log.Fatalf("Failed to retrieve test database: %v", err) 31 | } 32 | 33 | s, err := json.MarshalIndent(test, "", " ") 34 | if err != nil { 35 | log.Fatalf("Failed to marshal test database: %v", err) 36 | } 37 | 38 | fmt.Printf("%s\n", s) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/util/testing.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | ) 9 | 10 | func setupTestDir(t *testing.T) string { 11 | t.Helper() 12 | 13 | tmp, err := ioutil.TempDir("", "test") 14 | if err != nil { 15 | t.Fatalf("failed to create temp dir: %v", err) 16 | } 17 | 18 | files := []struct { 19 | name string 20 | content string 21 | }{ 22 | { 23 | name: "1.txt", 24 | content: "hello there", 25 | }, 26 | { 27 | name: "2.sql", 28 | content: "SELECT 1;", 29 | }, 30 | { 31 | name: "3.txt", 32 | content: "general kenobi", 33 | }, 34 | } 35 | 36 | for _, f := range files { 37 | if err := ioutil.WriteFile(path.Join(tmp, f.name), []byte(f.content), 0644); err != nil { 38 | t.Fatalf("failed to write test file %q: %v", f.name, err) 39 | } 40 | } 41 | 42 | return tmp 43 | } 44 | 45 | func cleanupTestDir(t *testing.T, path string) { 46 | t.Helper() 47 | 48 | if err := os.RemoveAll(path); err != nil { 49 | t.Logf("failed to remove test dir: %v", err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 aaa – all about apps GmbH | Nick Müller | Mario Ranftl and the "integresql" project contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /pkg/models/database_config.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | type DatabaseConfig struct { 10 | Host string `json:"host"` 11 | Port int `json:"port"` 12 | Username string `json:"username"` 13 | Password string `json:"password"` 14 | Database string `json:"database"` 15 | AdditionalParams map[string]string `json:"additionalParams,omitempty"` // Optional additional connection parameters mapped into the connection string 16 | } 17 | 18 | // Generates a connection string to be passed to sql.Open or equivalents, assuming Postgres syntax 19 | func (c DatabaseConfig) ConnectionString() string { 20 | var b strings.Builder 21 | b.WriteString(fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", c.Host, c.Port, c.Username, c.Password, c.Database)) 22 | 23 | if _, ok := c.AdditionalParams["sslmode"]; !ok { 24 | b.WriteString(" sslmode=disable") 25 | } 26 | 27 | if len(c.AdditionalParams) > 0 { 28 | params := make([]string, 0, len(c.AdditionalParams)) 29 | for param := range c.AdditionalParams { 30 | params = append(params, param) 31 | } 32 | 33 | sort.Strings(params) 34 | 35 | for _, param := range params { 36 | fmt.Fprintf(&b, " %s=%s", param, c.AdditionalParams[param]) 37 | } 38 | } 39 | 40 | return b.String() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/util/hash_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | ) 7 | 8 | func TestHashUtilGetDirectoryHash(t *testing.T) { 9 | t.Parallel() 10 | 11 | tmp := setupTestDir(t) 12 | defer cleanupTestDir(t, tmp) 13 | 14 | hash, err := GetDirectoryHash(tmp) 15 | if err != nil { 16 | t.Fatalf("failed to get directory hash: %v", err) 17 | } 18 | 19 | expected := "3c01b387636699191fdd281c78fcce8d" 20 | if hash != expected { 21 | t.Errorf("invalid directory hash, got %q, want %q", hash, expected) 22 | } 23 | } 24 | 25 | func TestHashUtilGetFileHash(t *testing.T) { 26 | t.Parallel() 27 | 28 | tmp := setupTestDir(t) 29 | defer cleanupTestDir(t, tmp) 30 | 31 | hash, err := GetFileHash(path.Join(tmp, "2.sql")) 32 | if err != nil { 33 | t.Fatalf("failed to get file hash: %v", err) 34 | } 35 | 36 | expected := "71568061b2970a4b7c5160fe75356e10" 37 | if hash != expected { 38 | t.Errorf("invalid file hash, got %q, want %q", hash, expected) 39 | } 40 | } 41 | 42 | func TestHashUtilGetTemplateHash(t *testing.T) { 43 | t.Parallel() 44 | 45 | tmp := setupTestDir(t) 46 | defer cleanupTestDir(t, tmp) 47 | 48 | hash, err := GetTemplateHash(tmp, path.Join(tmp, "2.sql")) 49 | if err != nil { 50 | t.Fatalf("failed to get template hash: %v", err) 51 | } 52 | 53 | expected := "addb861f5dc3ea8f45908a8b3cf7f969" 54 | if hash != expected { 55 | t.Errorf("invalid template hash, got %q, want %q", hash, expected) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # first is default task when running "make" without args 2 | build: format gobuild lint 3 | 4 | cli: format gobuild-cli lint 5 | 6 | format: 7 | go fmt 8 | 9 | gobuild: 10 | go build . 11 | 12 | gobuild-cli: 13 | go build -o ./bin/integresql-cli ./cmd/cli 14 | 15 | lint: 16 | golangci-lint run --fast 17 | 18 | # https://github.com/golang/go/issues/24573 19 | # w/o cache - see "go help testflag" 20 | # use https://github.com/kyoh86/richgo to color 21 | # note that these tests should not run verbose by default (e.g. use your IDE for this) 22 | # TODO: add test shuffling/seeding when landed in go v1.15 (https://github.com/golang/go/issues/28592) 23 | test: 24 | richgo test -cover -race -count=1 ./... 25 | 26 | init: modules tools tidy 27 | @go version 28 | 29 | # cache go modules (locally into .pkg) 30 | modules: 31 | go mod download 32 | 33 | # https://marcofranssen.nl/manage-go-tools-via-go-modules/ 34 | tools: 35 | cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % 36 | 37 | tidy: 38 | go mod tidy 39 | 40 | clean: 41 | rm -rf bin 42 | 43 | reset: 44 | @echo "DROP & CREATE database:" 45 | @echo " PGHOST=${PGHOST} PGDATABASE=${PGDATABASE}" PGUSER=${PGUSER} 46 | @echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ] 47 | psql -d postgres -c 'DROP DATABASE IF EXISTS "${PGDATABASE}";' 48 | psql -d postgres -c 'CREATE DATABASE "${PGDATABASE}" WITH OWNER ${PGUSER} TEMPLATE "template0"' 49 | 50 | # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html 51 | # ignore matching file/make rule combinations in working-dir 52 | .PHONY: test 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.2 AS development 2 | 3 | # https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md#walk-through 4 | ENV GOBIN /app/bin 5 | ENV PATH $GOBIN:$PATH 6 | 7 | # postgresql-support: Add the official postgres repo to install the matching postgresql-client tools of your stack 8 | # see https://wiki.postgresql.org/wiki/Apt 9 | # run lsb_release -c inside the container to pick the proper repository flavor 10 | # e.g. stretch=>stretch-pgdg, buster=>buster-pgdg 11 | RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main" \ 12 | | tee /etc/apt/sources.list.d/pgdg.list \ 13 | && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc \ 14 | | apt-key add - 15 | 16 | # Install required system dependencies 17 | RUN apt-get update \ 18 | && apt-get install -y --no-install-recommends \ 19 | locales \ 20 | postgresql-client-12 \ 21 | && apt-get clean \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | # vscode support: LANG must be supported, requires installing the locale package first 25 | # see https://github.com/Microsoft/vscode/issues/58015 26 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ 27 | dpkg-reconfigure --frontend=noninteractive locales && \ 28 | update-locale LANG=en_US.UTF-8 29 | 30 | ENV LANG en_US.UTF-8 31 | 32 | # sql-formatting: Install the same version of pg_formatter as used in your editors, as of 2020-03 thats v4.2 33 | # https://github.com/darold/pgFormatter/releases 34 | # https://github.com/bradymholt/vscode-pgFormatter/commits/master 35 | RUN wget https://github.com/darold/pgFormatter/archive/v4.2.tar.gz \ 36 | && tar xzf v4.2.tar.gz \ 37 | && cd pgFormatter-4.2 \ 38 | && perl Makefile.PL \ 39 | && make && make install 40 | 41 | # go richgo: (this package should NOT be installed via go get) 42 | # https://github.com/kyoh86/richgo/releases 43 | RUN wget https://github.com/kyoh86/richgo/releases/download/v0.3.3/richgo_0.3.3_linux_amd64.tar.gz \ 44 | && tar xzf richgo_0.3.3_linux_amd64.tar.gz \ 45 | && cp richgo /usr/local/bin/richgo 46 | 47 | # go linting: (this package should NOT be installed via go get) 48 | # https://github.com/golangci/golangci-lint#binary 49 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ 50 | | sh -s -- -b $(go env GOPATH)/bin v1.24.0 51 | 52 | # go swagger: (this package should NOT be installed via go get) 53 | # https://github.com/go-swagger/go-swagger/releases 54 | RUN curl -o /usr/local/bin/swagger -L'#' \ 55 | "https://github.com/go-swagger/go-swagger/releases/download/v0.23.0/swagger_linux_amd64" \ 56 | && chmod +x /usr/local/bin/swagger 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | 4 | integresql-client: 5 | build: 6 | context: . 7 | target: development 8 | working_dir: /app 9 | volumes: 10 | - .:/app #:delegated 11 | # - ./.pkg:/go/pkg # enable this to reuse the pkg cache 12 | depends_on: 13 | - postgres 14 | - integresql 15 | environment: &SERVICE_ENV 16 | PGDATABASE: &PSQL_DBNAME "sample" 17 | PGUSER: &PSQL_USER "dbuser" 18 | PGPASSWORD: &PSQL_PASS "testpass" 19 | PGHOST: &PSQL_HOST "postgres" 20 | PGPORT: &PSQL_PORT "5432" 21 | PGSSLMODE: &PSQL_SSLMODE "disable" 22 | 23 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 24 | cap_add: 25 | - SYS_PTRACE 26 | security_opt: 27 | - seccomp:unconfined 28 | 29 | # Overrides default command so things don't shut down after the process ends. 30 | command: /bin/sh -c "while sleep 1000; do :; done" 31 | 32 | postgres: 33 | image: postgres:12.2-alpine # should be the same version as used in .drone.yml, Dockerfile and live 34 | command: "postgres -c 'shared_buffers=128MB' -c 'fsync=off' -c 'synchronous_commit=off' -c 'full_page_writes=off' -c 'max_connections=100' -c 'client_min_messages=warning'" 35 | expose: 36 | - "5432" 37 | ports: 38 | - "5432:5432" 39 | environment: 40 | POSTGRES_DB: *PSQL_DBNAME 41 | POSTGRES_USER: *PSQL_USER 42 | POSTGRES_PASSWORD: *PSQL_PASS 43 | volumes: 44 | - pgvolume:/var/lib/postgresql/data 45 | 46 | integresql: 47 | image: allaboutapps/integresql:latest 48 | expose: 49 | - "5000" 50 | ports: 51 | - "5000:5000" 52 | depends_on: 53 | - postgres 54 | environment: 55 | PGDATABASE: *PSQL_DBNAME 56 | PGUSER: *PSQL_USER 57 | PGPASSWORD: *PSQL_PASS 58 | PGHOST: *PSQL_HOST 59 | PGPORT: *PSQL_PORT 60 | 61 | # # Only relevant if you want to attach a running integresql docker-compose network 62 | # # Typically this is running on the "integresql_default" network 63 | # integresql-client: 64 | # build: 65 | # context: . 66 | # target: development 67 | # working_dir: /app 68 | # volumes: 69 | # - .:/app #:delegated 70 | # environment: &SERVICE_ENV 71 | # PGDATABASE: &PSQL_DBNAME "sample" 72 | # PGUSER: &PSQL_USER "dbuser" 73 | # PGPASSWORD: &PSQL_PASS "testpass" 74 | # PGHOST: &PSQL_HOST "postgres" 75 | # PGPORT: &PSQL_PORT "5432" 76 | # PGSSLMODE: &PSQL_SSLMODE "disable" 77 | # networks: 78 | # - integresql_default 79 | 80 | volumes: 81 | pgvolume: # declare a named volume to persist DB data 82 | 83 | # # Only relevant if you want to attach a running integresql docker-compose network 84 | # # Typically this is running on the "integresql_default" network 85 | # networks: 86 | # integresql_default: 87 | # external: true -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.106.0/containers/docker-existing-docker-compose 3 | // If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. 4 | { 5 | "name": "app", 6 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 7 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 8 | "dockerComposeFile": [ 9 | "../docker-compose.yml" 10 | ], 11 | // The 'service' property is the name of the service for the container that VS Code should 12 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 13 | "service": "integresql-client", 14 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 15 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 16 | "workspaceFolder": "/app", 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "terminal.integrated.shell.linux": null, 20 | // https://github.com/golang/tools/blob/master/gopls/doc/vscode.md#vscode 21 | "go.useLanguageServer": true, 22 | "[go]": { 23 | "editor.formatOnSave": true, 24 | "editor.codeActionsOnSave": { 25 | "source.organizeImports": true, 26 | }, 27 | // Optional: Disable snippets, as they conflict with completion ranking. 28 | "editor.snippetSuggestions": "none", 29 | }, 30 | "[go.mod]": { 31 | "editor.formatOnSave": true, 32 | "editor.codeActionsOnSave": { 33 | "source.organizeImports": true, 34 | }, 35 | }, 36 | "[sql]": { 37 | "editor.formatOnSave": true 38 | }, 39 | "gopls": { 40 | // Add parameter placeholders when completing a function. 41 | "usePlaceholders": true, 42 | // If true, enable additional analyses with staticcheck. 43 | // Warning: This will significantly increase memory usage. 44 | // DISABLED, done via 45 | "staticcheck": false, 46 | }, 47 | // https://code.visualstudio.com/docs/languages/go#_intellisense 48 | "go.autocompleteUnimportedPackages": true, 49 | // https://github.com/golangci/golangci-lint#editor-integration 50 | "go.lintTool": "golangci-lint", 51 | "go.lintFlags": [ 52 | "--fast" 53 | ], 54 | // disable test caching, race and show coverage (in sync with makefile) 55 | "go.testFlags": [ 56 | "-cover", 57 | "-race", 58 | "-count=1", 59 | "-v" 60 | ], 61 | // "go.lintOnSave": "workspace" 62 | // general build settings in sync with our makefile 63 | // "go.buildFlags": [ 64 | // "-o", 65 | // "bin/app" 66 | // ] 67 | // "sqltools.connections": [ 68 | // { 69 | // "database": "sample", 70 | // "dialect": "PostgreSQL", 71 | // "name": "postgres", 72 | // "password": "9bed16f749d74a3c8bfbced18a7647f5", 73 | // "port": 5432, 74 | // "server": "postgres", 75 | // "username": "dbuser" 76 | // } 77 | // ], 78 | // "sqltools.autoConnectTo": [ 79 | // "postgres" 80 | // ], 81 | // // only use pg_format to actually format! 82 | // "sqltools.formatLanguages": [], 83 | // "sqltools.telemetry": false, 84 | // "sqltools.autoOpenSessionFiles": false 85 | }, 86 | // Add the IDs of extensions you want installed when the container is created. 87 | "extensions": [ 88 | // required: 89 | "ms-vscode.go", 90 | "bradymholt.pgformatter", 91 | // optional: 92 | // "766b.go-outliner", 93 | "heaths.vscode-guid", 94 | "bungcip.better-toml", 95 | "eamodio.gitlens", 96 | "casualjim.gotemplate" 97 | // "mtxr.sqltools", 98 | ] 99 | // Uncomment the next line if you want start specific services in your Docker Compose config. 100 | // "runServices": [], 101 | // Uncomment the next line if you want to keep your containers running after VS Code shuts down. 102 | // "shutdownAction": "none", 103 | // Uncomment the next line to run commands after the container is created - for example installing git. 104 | // "postCreateCommand": "apt-get update && apt-get install -y git", 105 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 106 | // "remoteUser": "vscode" 107 | } -------------------------------------------------------------------------------- /pkg/util/hash.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "sync" 12 | ) 13 | 14 | // Taken from https://blog.golang.org/pipelines/parallel.go @ 2020-04-07T13:03:47+00:00 15 | 16 | // A result is the product of reading and summing a file using MD5. 17 | type result struct { 18 | path string 19 | sum [md5.Size]byte 20 | err error 21 | } 22 | 23 | // sumFiles starts goroutines to walk the directory tree at root and digest each 24 | // regular file. These goroutines send the results of the digests on the result 25 | // channel and send the result of the walk on the error channel. If done is 26 | // closed, sumFiles abandons its work. 27 | func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) { 28 | // For each regular file, start a goroutine that sums the file and sends 29 | // the result on c. Send the result of the walk on errc. 30 | c := make(chan result) 31 | errc := make(chan error, 1) 32 | go func() { // HL 33 | var wg sync.WaitGroup 34 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 35 | if err != nil { 36 | return err 37 | } 38 | if !info.Mode().IsRegular() { 39 | return nil 40 | } 41 | wg.Add(1) 42 | go func() { // HL 43 | data, err := ioutil.ReadFile(path) 44 | select { 45 | case c <- result{path, md5.Sum(data), err}: // HL 46 | case <-done: // HL 47 | } 48 | wg.Done() 49 | }() 50 | // Abort the walk if done is closed. 51 | select { 52 | case <-done: // HL 53 | return errors.New("walk canceled") 54 | default: 55 | return nil 56 | } 57 | }) 58 | // Walk has returned, so all calls to wg.Add are done. Start a 59 | // goroutine to close c once all the sends are done. 60 | go func() { // HL 61 | wg.Wait() 62 | close(c) // HL 63 | }() 64 | // No select needed here, since errc is buffered. 65 | errc <- err // HL 66 | }() 67 | return c, errc 68 | } 69 | 70 | // MD5All reads all the files in the file tree rooted at root and returns a map 71 | // from file path to the MD5 sum of the file's contents. If the directory walk 72 | // fails or any read operation fails, MD5All returns an error. In that case, 73 | // MD5All does not wait for inflight read operations to complete. 74 | func MD5All(root string) (map[string][md5.Size]byte, error) { 75 | // MD5All closes the done channel when it returns; it may do so before 76 | // receiving all the values from c and errc. 77 | done := make(chan struct{}) // HLdone 78 | defer close(done) // HLdone 79 | 80 | c, errc := sumFiles(done, root) // HLdone 81 | 82 | m := make(map[string][md5.Size]byte) 83 | for r := range c { // HLrange 84 | if r.err != nil { 85 | return nil, r.err 86 | } 87 | m[r.path] = r.sum 88 | } 89 | if err := <-errc; err != nil { 90 | return nil, err 91 | } 92 | return m, nil 93 | } 94 | 95 | // GetDirectoryHash returns a MD5 sum of a directory, calculated as the hash 96 | // of all contained files' hashes. If walking the directory or any read 97 | // operation fails, GetDirectoryHash returns an error. In that case, 98 | // GetDirectoryHash does not wait for inflight read operations to complete. 99 | func GetDirectoryHash(dirPath string) (string, error) { 100 | m, err := MD5All(dirPath) 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | h := md5.New() 106 | 107 | var paths []string 108 | for path := range m { 109 | paths = append(paths, path) 110 | } 111 | sort.Strings(paths) 112 | 113 | for _, path := range paths { 114 | fmt.Fprintf(h, "%x", m[path]) 115 | } 116 | 117 | return fmt.Sprintf("%x", h.Sum(nil)), nil 118 | } 119 | 120 | // GetFileHash returns a MD5 sum of a file, calculated using the file's content 121 | func GetFileHash(filePath string) (string, error) { 122 | data, err := ioutil.ReadFile(filePath) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | return fmt.Sprintf("%x", md5.Sum(data)), nil 128 | } 129 | 130 | func GetTemplateHash(paths ...string) (string, error) { 131 | h := md5.New() 132 | 133 | for _, p := range paths { 134 | f, err := os.Stat(p) 135 | if err != nil { 136 | return "", err 137 | } 138 | 139 | var hash string 140 | switch m := f.Mode(); { 141 | case m.IsDir(): 142 | hash, err = GetDirectoryHash(p) 143 | case m.IsRegular(): 144 | hash, err = GetFileHash(p) 145 | default: 146 | return "", errors.New("invalid file mode for path, cannot generate hash") 147 | } 148 | 149 | if err != nil { 150 | return "", err 151 | } 152 | 153 | fmt.Fprintf(h, "%s", hash) 154 | } 155 | 156 | return fmt.Sprintf("%x", h.Sum(nil)), nil 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IntegreSQL Client Library for Golang 2 | 3 | Client library for interacting with a [`IntegreSQL` server](https://github.com/allaboutapps/integresql), managing isolated PostgreSQL databases for your integration tests. 4 | 5 | ## Overview [![](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/allaboutapps/integresql-client-go?tab=doc) [![](https://goreportcard.com/badge/github.com/allaboutapps/integresql-client-go)](https://goreportcard.com/report/github.com/allaboutapps/integresql-client-go) ![](https://github.com/allaboutapps/integresql-client-go/workflows/build/badge.svg?branch=master) 6 | 7 | ## Table of Contents 8 | 9 | - [Background](#background) 10 | - [Install](#install) 11 | - [Configuration](#configuration) 12 | - [Usage](#usage) 13 | - [Contributing](#contributing) 14 | - [Development setup](#development-setup) 15 | - [Development quickstart](#development-quickstart) 16 | - [Maintainers](#maintainers) 17 | - [License](#license) 18 | 19 | ## Background 20 | 21 | See [IntegreSQL: Background](https://github.com/allaboutapps/integresql#background) 22 | 23 | ## Install 24 | 25 | Install the `IntegreSQL` client for Go using `go get` or by simply importing the library in your testing code (go modules, see below): 26 | 27 | ```bash 28 | go get github.com/allaboutapps/integresql-client-go 29 | ``` 30 | 31 | ## Configuration 32 | 33 | The `IntegreSQL` client library requires little configuration which can either be passed via the `ClientConfig` struct or parsed from environment variables automatically. The following settings are available: 34 | 35 | | Description | Environment variable | Default | Required | 36 | | ---------------------------------------------------------- | ------------------------------- | ------------------------------ | -------- | 37 | | IntegreSQL: base URL of server `http://127.0.0.1:5000/api` | `INTEGRESQL_CLIENT_BASE_URL` | `"http://integresql:5000/api"` | | 38 | | IntegreSQL: API version of server | `INTEGRESQL_CLIENT_API_VERSION` | `"v1"` | | 39 | 40 | 41 | ## Usage 42 | 43 | If you want to take a look on how we integrate IntegreSQL - 🤭 - please just try our [go-starter](https://github.com/allaboutapps/go-starter) project or take a look at our [testing setup code](https://github.com/allaboutapps/go-starter/blob/master/internal/test/testing.go). 44 | 45 | In general setting up the `IntegreSQL` client, initializing a PostgreSQL template (migrate + seed) and retrieving a PostgreSQL test database goes like this: 46 | 47 | ```go 48 | package yourpkg 49 | 50 | import ( 51 | "github.com/allaboutapps/integresql-client-go" 52 | "github.com/allaboutapps/integresql-client-go/pkg/util" 53 | ) 54 | 55 | func doStuff() error { 56 | c, err := integresql.DefaultClientFromEnv() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // compute a hash over all database related files in your workspace (warm template cache) 62 | hash, err := hash.GetTemplateHash("/app/scripts/migrations", "/app/internal/fixtures/fixtures.go") 63 | if err != nil { 64 | return err 65 | } 66 | 67 | template, err := c.InitializeTemplate(context.TODO(), hash) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Use template database config received to initialize template 73 | // e.g. by applying migrations and fixtures 74 | 75 | if err := c.FinalizeTemplate(context.TODO(), hash); err != nil { 76 | return err 77 | } 78 | 79 | test, err := c.GetTestDatabase(context.TODO(), hash) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // Use test database config received to run integration tests in isolated DB 85 | } 86 | ``` 87 | 88 | A very basic example has been added as the `cmd/cli` executable, you can build it using `make cli` and execute `integresql-cli` afterwards. 89 | 90 | ## Contributing 91 | 92 | Pull requests are welcome. For major changes, please [open an issue](https://github.com/allaboutapps/integresql/issues/new) first to discuss what you would like to change. 93 | 94 | Please make sure to update tests as appropriate. 95 | 96 | ### Development setup 97 | 98 | `IntegreSQL` requires the following local setup for development: 99 | 100 | - [Docker CE](https://docs.docker.com/install/) (19.03 or above) 101 | - [Docker Compose](https://docs.docker.com/compose/install/) (1.25 or above) 102 | 103 | The project makes use of the [devcontainer functionality](https://code.visualstudio.com/docs/remote/containers) provided by [Visual Studio Code](https://code.visualstudio.com/) so no local installation of a Go compiler is required when using VSCode as an IDE. 104 | 105 | Should you prefer to develop the `IntegreSQL` client library without the Docker setup, please ensure a working [Go](https://golang.org/dl/) (1.14 or above) environment has been configured as well as an `IntegreSQL` server and a a PostgreSQL instance are available (tested against PostgreSQL version 12 or above, but *should* be compatible to lower versions) and the appropriate environment variables have been configured as described in the [Install](#install) section. 106 | 107 | ### Development quickstart 108 | 109 | 1. Start the local docker-compose setup and open an interactive shell in the development container: 110 | 111 | ```bash 112 | # Build the development Docker container, start it and open a shell 113 | ./docker-helper.sh --up 114 | ``` 115 | 116 | 2. Initialize the project, downloading all dependencies and tools required (executed within the dev container): 117 | 118 | ```bash 119 | # Init dependencies/tools 120 | make init 121 | 122 | # Build executable (generate, format, build, vet) 123 | make 124 | ``` 125 | 126 | 3. Execute project tests: 127 | 128 | ```bash 129 | # Execute tests 130 | make test 131 | ``` 132 | 133 | ## Maintainers 134 | 135 | - [Nick Müller - @MorpheusXAUT](https://github.com/MorpheusXAUT) 136 | - [Mario Ranftl - @majodev](https://github.com/majodev) 137 | 138 | ## License 139 | 140 | [MIT](LICENSE) © 2020 aaa – all about apps GmbH | Nick Müller | Mario Ranftl and the `IntegreSQL` project contributors -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package integresql 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "path" 14 | 15 | "github.com/allaboutapps/integresql-client-go/pkg/models" 16 | _ "github.com/lib/pq" 17 | ) 18 | 19 | var ( 20 | ErrManagerNotReady = errors.New("manager not ready") 21 | ErrTemplateAlreadyInitialized = errors.New("template is already initialized") 22 | ErrTemplateNotFound = errors.New("template not found") 23 | ErrDatabaseDiscarded = errors.New("database was discarded (typically failed during initialize/finalize)") 24 | ErrTestNotFound = errors.New("test database not found") 25 | ) 26 | 27 | type Client struct { 28 | baseURL *url.URL 29 | client *http.Client 30 | config ClientConfig 31 | } 32 | 33 | func NewClient(config ClientConfig) (*Client, error) { 34 | c := &Client{ 35 | baseURL: nil, 36 | client: nil, 37 | config: config, 38 | } 39 | 40 | defaultConfig := DefaultClientConfigFromEnv() 41 | 42 | if len(c.config.BaseURL) == 0 { 43 | c.config.BaseURL = defaultConfig.BaseURL 44 | } 45 | 46 | if len(c.config.APIVersion) == 0 { 47 | c.config.APIVersion = defaultConfig.APIVersion 48 | } 49 | 50 | u, err := url.Parse(c.config.BaseURL) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | c.baseURL = u.ResolveReference(&url.URL{Path: path.Join(u.Path, c.config.APIVersion)}) 56 | 57 | c.client = &http.Client{} 58 | 59 | return c, nil 60 | } 61 | 62 | func DefaultClientFromEnv() (*Client, error) { 63 | return NewClient(DefaultClientConfigFromEnv()) 64 | } 65 | 66 | func (c *Client) ResetAllTracking(ctx context.Context) error { 67 | req, err := c.newRequest(ctx, "DELETE", "/admin/templates", nil) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | var msg string 73 | resp, err := c.do(req, &msg) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if resp.StatusCode != http.StatusNoContent { 79 | return fmt.Errorf("failed to reset all tracking: %v", msg) 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (c *Client) InitializeTemplate(ctx context.Context, hash string) (models.TemplateDatabase, error) { 86 | var template models.TemplateDatabase 87 | 88 | payload := map[string]string{"hash": hash} 89 | 90 | req, err := c.newRequest(ctx, "POST", "/templates", payload) 91 | if err != nil { 92 | return template, err 93 | } 94 | 95 | resp, err := c.do(req, &template) 96 | if err != nil { 97 | return template, err 98 | } 99 | 100 | switch resp.StatusCode { 101 | case http.StatusOK: 102 | return template, nil 103 | case http.StatusLocked: 104 | return template, ErrTemplateAlreadyInitialized 105 | case http.StatusServiceUnavailable: 106 | return template, ErrManagerNotReady 107 | default: 108 | return template, fmt.Errorf("received unexpected HTTP status %d (%s)", resp.StatusCode, resp.Status) 109 | } 110 | } 111 | 112 | func (c *Client) SetupTemplate(ctx context.Context, hash string, init func(conn string) error) error { 113 | template, err := c.InitializeTemplate(ctx, hash) 114 | if err == nil { 115 | if err := init(template.Config.ConnectionString()); err != nil { 116 | return err 117 | } 118 | 119 | return c.FinalizeTemplate(ctx, hash) 120 | } else if err == ErrTemplateAlreadyInitialized { 121 | return nil 122 | } else { 123 | return err 124 | } 125 | } 126 | 127 | func (c *Client) SetupTemplateWithDBClient(ctx context.Context, hash string, init func(db *sql.DB) error) error { 128 | template, err := c.InitializeTemplate(ctx, hash) 129 | if err == nil { 130 | db, err := sql.Open("postgres", template.Config.ConnectionString()) 131 | if err != nil { 132 | return err 133 | } 134 | defer db.Close() 135 | 136 | if err := db.PingContext(ctx); err != nil { 137 | return err 138 | } 139 | 140 | if err := init(db); err != nil { 141 | return err 142 | } 143 | 144 | return c.FinalizeTemplate(ctx, hash) 145 | } else if err == ErrTemplateAlreadyInitialized { 146 | return nil 147 | } else { 148 | return err 149 | } 150 | } 151 | 152 | func (c *Client) DiscardTemplate(ctx context.Context, hash string) error { 153 | req, err := c.newRequest(ctx, "DELETE", fmt.Sprintf("/templates/%s", hash), nil) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | resp, err := c.do(req, nil) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | switch resp.StatusCode { 164 | case http.StatusNoContent: 165 | return nil 166 | case http.StatusNotFound: 167 | return ErrTemplateNotFound 168 | case http.StatusServiceUnavailable: 169 | return ErrManagerNotReady 170 | default: 171 | return fmt.Errorf("received unexpected HTTP status %d (%s)", resp.StatusCode, resp.Status) 172 | } 173 | } 174 | 175 | func (c *Client) FinalizeTemplate(ctx context.Context, hash string) error { 176 | req, err := c.newRequest(ctx, "PUT", fmt.Sprintf("/templates/%s", hash), nil) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | resp, err := c.do(req, nil) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | switch resp.StatusCode { 187 | case http.StatusNoContent: 188 | return nil 189 | case http.StatusNotFound: 190 | return ErrTemplateNotFound 191 | case http.StatusServiceUnavailable: 192 | return ErrManagerNotReady 193 | default: 194 | return fmt.Errorf("received unexpected HTTP status %d (%s)", resp.StatusCode, resp.Status) 195 | } 196 | } 197 | 198 | func (c *Client) GetTestDatabase(ctx context.Context, hash string) (models.TestDatabase, error) { 199 | var test models.TestDatabase 200 | 201 | req, err := c.newRequest(ctx, "GET", fmt.Sprintf("/templates/%s/tests", hash), nil) 202 | if err != nil { 203 | return test, err 204 | } 205 | 206 | resp, err := c.do(req, &test) 207 | if err != nil { 208 | return test, err 209 | } 210 | 211 | switch resp.StatusCode { 212 | case http.StatusOK: 213 | return test, nil 214 | case http.StatusNotFound: 215 | return test, ErrTemplateNotFound 216 | case http.StatusGone: 217 | return test, ErrDatabaseDiscarded 218 | case http.StatusServiceUnavailable: 219 | return test, ErrManagerNotReady 220 | default: 221 | return test, fmt.Errorf("received unexpected HTTP status %d (%s)", resp.StatusCode, resp.Status) 222 | } 223 | } 224 | 225 | func (c *Client) ReturnTestDatabase(ctx context.Context, hash string, id int) error { 226 | req, err := c.newRequest(ctx, "DELETE", fmt.Sprintf("/templates/%s/tests/%d", hash, id), nil) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | resp, err := c.do(req, nil) 232 | if err != nil { 233 | return err 234 | } 235 | 236 | switch resp.StatusCode { 237 | case http.StatusNoContent: 238 | return nil 239 | case http.StatusNotFound: 240 | return ErrTemplateNotFound 241 | case http.StatusServiceUnavailable: 242 | return ErrManagerNotReady 243 | default: 244 | return fmt.Errorf("received unexpected HTTP status %d (%s)", resp.StatusCode, resp.Status) 245 | } 246 | } 247 | 248 | func (c *Client) newRequest(ctx context.Context, method string, endpoint string, body interface{}) (*http.Request, error) { 249 | u := c.baseURL.ResolveReference(&url.URL{Path: path.Join(c.baseURL.Path, endpoint)}) 250 | 251 | var buf io.ReadWriter 252 | if body != nil { 253 | buf = new(bytes.Buffer) 254 | if err := json.NewEncoder(buf).Encode(body); err != nil { 255 | return nil, err 256 | } 257 | } 258 | 259 | req, err := http.NewRequestWithContext(ctx, method, u.String(), buf) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | if body != nil { 265 | req.Header.Set("Content-Type", "application/json; charset=UTF-8") 266 | } 267 | 268 | req.Header.Set("Accept", "application/json") 269 | 270 | return req, nil 271 | } 272 | 273 | func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) { 274 | resp, err := c.client.Do(req) 275 | if err != nil { 276 | return nil, err 277 | } 278 | 279 | // body must always be closed 280 | defer resp.Body.Close() 281 | 282 | if resp.StatusCode == http.StatusAccepted || resp.StatusCode == http.StatusNoContent { 283 | return resp, nil 284 | } 285 | 286 | // if the provided v pointer is nil we cannot unmarschal the body to anything 287 | if v == nil { 288 | return resp, nil 289 | } 290 | 291 | if err := json.NewDecoder(resp.Body).Decode(v); err != nil { 292 | return nil, err 293 | } 294 | 295 | return resp, err 296 | } 297 | -------------------------------------------------------------------------------- /pkg/models/database_config_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | // Using table driven tests as described here: https://github.com/golang/go/wiki/TableDrivenTests#parallel-testing 9 | func TestDatabaseConfigConnectionString(t *testing.T) { 10 | t.Parallel() // marks table driven test execution function as capable of running in parallel with other tests 11 | 12 | tests := []struct { 13 | name string 14 | config DatabaseConfig 15 | want string 16 | }{ 17 | { 18 | name: "Simple", 19 | config: DatabaseConfig{ 20 | Host: "localhost", 21 | Port: 5432, 22 | Username: "simple", 23 | Password: "database_config", 24 | Database: "simple_database_config", 25 | }, 26 | want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=disable", 27 | }, 28 | { 29 | name: "SSLMode", 30 | config: DatabaseConfig{ 31 | Host: "localhost", 32 | Port: 5432, 33 | Username: "simple", 34 | Password: "database_config", 35 | Database: "simple_database_config", 36 | AdditionalParams: map[string]string{ 37 | "sslmode": "prefer", 38 | }, 39 | }, 40 | want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=prefer", 41 | }, 42 | { 43 | name: "Complex", 44 | config: DatabaseConfig{ 45 | Host: "localhost", 46 | Port: 5432, 47 | Username: "simple", 48 | Password: "database_config", 49 | Database: "simple_database_config", 50 | AdditionalParams: map[string]string{ 51 | "connect_timeout": "10", 52 | "sslmode": "verify-full", 53 | "sslcert": "/app/certs/pg.pem", 54 | "sslkey": "/app/certs/pg.key", 55 | "sslrootcert": "/app/certs/pg_root.pem", 56 | }, 57 | }, 58 | want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config connect_timeout=10 sslcert=/app/certs/pg.pem sslkey=/app/certs/pg.key sslmode=verify-full sslrootcert=/app/certs/pg_root.pem", 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 64 | t.Run(tt.name, func(t *testing.T) { 65 | t.Parallel() // marks each test case as capable of running in parallel with each other 66 | 67 | if got := tt.config.ConnectionString(); got != tt.want { 68 | t.Errorf("invalid connection string, got %q, want %q", got, tt.want) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestDatabaseConfigMarshal(t *testing.T) { 75 | t.Parallel() // marks table driven test execution function as capable of running in parallel with other tests 76 | 77 | tests := []struct { 78 | name string 79 | config DatabaseConfig 80 | want string 81 | }{ 82 | { 83 | name: "Simple", 84 | config: DatabaseConfig{ 85 | Host: "localhost", 86 | Port: 5432, 87 | Username: "simple", 88 | Password: "database_config", 89 | Database: "simple_database_config", 90 | }, 91 | want: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\"}", 92 | }, 93 | { 94 | name: "SSLMode", 95 | config: DatabaseConfig{ 96 | Host: "localhost", 97 | Port: 5432, 98 | Username: "simple", 99 | Password: "database_config", 100 | Database: "simple_database_config", 101 | AdditionalParams: map[string]string{ 102 | "sslmode": "prefer", 103 | }, 104 | }, 105 | want: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\",\"additionalParams\":{\"sslmode\":\"prefer\"}}", 106 | }, 107 | { 108 | name: "Complex", 109 | config: DatabaseConfig{ 110 | Host: "localhost", 111 | Port: 5432, 112 | Username: "simple", 113 | Password: "database_config", 114 | Database: "simple_database_config", 115 | AdditionalParams: map[string]string{ 116 | "connect_timeout": "10", 117 | "sslmode": "verify-full", 118 | "sslcert": "/app/certs/pg.pem", 119 | "sslkey": "/app/certs/pg.key", 120 | "sslrootcert": "/app/certs/pg_root.pem", 121 | }, 122 | }, 123 | want: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\",\"additionalParams\":{\"connect_timeout\":\"10\",\"sslcert\":\"/app/certs/pg.pem\",\"sslkey\":\"/app/certs/pg.key\",\"sslmode\":\"verify-full\",\"sslrootcert\":\"/app/certs/pg_root.pem\"}}", 124 | }, 125 | } 126 | 127 | for _, tt := range tests { 128 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 129 | t.Run(tt.name, func(t *testing.T) { 130 | t.Parallel() // marks each test case as capable of running in parallel with each other 131 | 132 | got, err := json.Marshal(tt.config) 133 | if err != nil { 134 | t.Fatalf("failed to marshal database config: %v", err) 135 | } 136 | 137 | if string(got) != tt.want { 138 | t.Errorf("invalid JSON string, got %q, want %q", got, tt.want) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | func TestDatabaseConfigUnmarshal(t *testing.T) { 145 | t.Parallel() // marks table driven test execution function as capable of running in parallel with other tests 146 | 147 | tests := []struct { 148 | name string 149 | want DatabaseConfig 150 | json string 151 | }{ 152 | { 153 | name: "Simple", 154 | want: DatabaseConfig{ 155 | Host: "localhost", 156 | Port: 5432, 157 | Username: "simple", 158 | Password: "database_config", 159 | Database: "simple_database_config", 160 | }, 161 | json: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\"}", 162 | }, 163 | { 164 | name: "SSLMode", 165 | want: DatabaseConfig{ 166 | Host: "localhost", 167 | Port: 5432, 168 | Username: "simple", 169 | Password: "database_config", 170 | Database: "simple_database_config", 171 | AdditionalParams: map[string]string{ 172 | "sslmode": "prefer", 173 | }, 174 | }, 175 | json: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\",\"additionalParams\":{\"sslmode\":\"prefer\"}}", 176 | }, 177 | { 178 | name: "Complex", 179 | want: DatabaseConfig{ 180 | Host: "localhost", 181 | Port: 5432, 182 | Username: "simple", 183 | Password: "database_config", 184 | Database: "simple_database_config", 185 | AdditionalParams: map[string]string{ 186 | "connect_timeout": "10", 187 | "sslmode": "verify-full", 188 | "sslcert": "/app/certs/pg.pem", 189 | "sslkey": "/app/certs/pg.key", 190 | "sslrootcert": "/app/certs/pg_root.pem", 191 | }, 192 | }, 193 | json: "{\"host\":\"localhost\",\"port\":5432,\"username\":\"simple\",\"password\":\"database_config\",\"database\":\"simple_database_config\",\"additionalParams\":{\"connect_timeout\":\"10\",\"sslcert\":\"/app/certs/pg.pem\",\"sslkey\":\"/app/certs/pg.key\",\"sslmode\":\"verify-full\",\"sslrootcert\":\"/app/certs/pg_root.pem\"}}", 194 | }, 195 | } 196 | 197 | for _, tt := range tests { 198 | tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 199 | t.Run(tt.name, func(t *testing.T) { 200 | t.Parallel() // marks each test case as capable of running in parallel with each other 201 | 202 | var got DatabaseConfig 203 | if err := json.Unmarshal([]byte(tt.json), &got); err != nil { 204 | t.Fatalf("failed to unmarshal database config: %v", err) 205 | } 206 | 207 | if got.Host != tt.want.Host { 208 | t.Errorf("invalid host, got %q, want %q", got.Host, tt.want.Host) 209 | } 210 | if got.Port != tt.want.Port { 211 | t.Errorf("invalid port, got %d, want %d", got.Port, tt.want.Port) 212 | } 213 | if got.Username != tt.want.Username { 214 | t.Errorf("invalid username, got %q, want %q", got.Username, tt.want.Username) 215 | } 216 | if got.Password != tt.want.Password { 217 | t.Errorf("invalid password, got %q, want %q", got.Password, tt.want.Password) 218 | } 219 | if got.Database != tt.want.Database { 220 | t.Errorf("invalid database, got %q, want %q", got.Database, tt.want.Database) 221 | } 222 | 223 | for k, v := range tt.want.AdditionalParams { 224 | g, ok := got.AdditionalParams[k] 225 | if !ok { 226 | t.Errorf("invalid additional parameter %q, got , want %q", k, v) 227 | continue 228 | } 229 | 230 | if g != v { 231 | t.Errorf("invalid additional parameter %q, got %q, want %q", k, g, v) 232 | } 233 | } 234 | }) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package integresql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "sync" 8 | "testing" 9 | 10 | _ "github.com/lib/pq" 11 | ) 12 | 13 | func TestClientInitializeTemplate(t *testing.T) { 14 | ctx := context.Background() 15 | 16 | c, err := DefaultClientFromEnv() 17 | if err != nil { 18 | t.Fatalf("failed to create client: %v", err) 19 | } 20 | 21 | if err := c.ResetAllTracking(ctx); err != nil { 22 | t.Fatalf("failed to reset all test pool tracking: %v", err) 23 | } 24 | 25 | hash := "hashinghash1" 26 | 27 | template, err := c.InitializeTemplate(ctx, hash) 28 | if err != nil { 29 | t.Fatalf("failed to initialize template: %v", err) 30 | } 31 | 32 | if len(template.Config.Database) == 0 { 33 | t.Error("received invalid template database config") 34 | } 35 | } 36 | 37 | func TestClientDiscardTemplate(t *testing.T) { 38 | ctx := context.Background() 39 | 40 | c, err := DefaultClientFromEnv() 41 | if err != nil { 42 | t.Fatalf("failed to create client: %v", err) 43 | } 44 | 45 | if err := c.ResetAllTracking(ctx); err != nil { 46 | t.Fatalf("failed to reset all test pool tracking: %v", err) 47 | } 48 | 49 | hash := "hashinghash2" 50 | 51 | if _, err := c.InitializeTemplate(ctx, hash); err != nil { 52 | t.Fatalf("failed to initialize template: %v", err) 53 | } 54 | 55 | if err := c.DiscardTemplate(ctx, hash); err != nil { 56 | t.Fatalf("failed to discard template: %v", err) 57 | } 58 | 59 | if _, err := c.InitializeTemplate(ctx, hash); err != nil { 60 | t.Fatalf("failed to reinitialize template: %v", err) 61 | } 62 | 63 | if err := c.FinalizeTemplate(ctx, hash); err != nil { 64 | t.Fatalf("failed to refinalize template: %v", err) 65 | } 66 | } 67 | 68 | func TestClientFinalizeTemplate(t *testing.T) { 69 | ctx := context.Background() 70 | 71 | c, err := DefaultClientFromEnv() 72 | if err != nil { 73 | t.Fatalf("failed to create client: %v", err) 74 | } 75 | 76 | if err := c.ResetAllTracking(ctx); err != nil { 77 | t.Fatalf("failed to reset all test pool tracking: %v", err) 78 | } 79 | 80 | hash := "hashinghash2" 81 | 82 | if _, err := c.InitializeTemplate(ctx, hash); err != nil { 83 | t.Fatalf("failed to initialize template: %v", err) 84 | } 85 | 86 | if err := c.FinalizeTemplate(ctx, hash); err != nil { 87 | t.Fatalf("failed to finalize template: %v", err) 88 | } 89 | } 90 | 91 | func TestClientGetTestDatabase(t *testing.T) { 92 | ctx := context.Background() 93 | 94 | c, err := DefaultClientFromEnv() 95 | if err != nil { 96 | t.Fatalf("failed to create client: %v", err) 97 | } 98 | 99 | if err := c.ResetAllTracking(ctx); err != nil { 100 | t.Fatalf("failed to reset all test pool tracking: %v", err) 101 | } 102 | 103 | hash := "hashinghash3" 104 | 105 | if _, err := c.InitializeTemplate(ctx, hash); err != nil { 106 | t.Fatalf("failed to initialize template: %v", err) 107 | } 108 | 109 | if err := c.FinalizeTemplate(ctx, hash); err != nil { 110 | t.Fatalf("failed to finalize template: %v", err) 111 | } 112 | 113 | test, err := c.GetTestDatabase(ctx, hash) 114 | if err != nil { 115 | t.Fatalf("failed to get test database: %v", err) 116 | } 117 | 118 | if test.TemplateHash != hash { 119 | t.Errorf("test database has invalid template hash, got %q, want %q", test.TemplateHash, hash) 120 | } 121 | 122 | db, err := sql.Open("postgres", test.Config.ConnectionString()) 123 | if err != nil { 124 | t.Fatalf("failed to open test database connection: %v", err) 125 | } 126 | defer db.Close() 127 | 128 | if err := db.Ping(); err != nil { 129 | t.Fatalf("failed to ping test database connection: %v", err) 130 | } 131 | 132 | test2, err := c.GetTestDatabase(ctx, hash) 133 | if err != nil { 134 | t.Fatalf("failed to get second test database: %v", err) 135 | } 136 | 137 | if test2.TemplateHash != hash { 138 | t.Errorf("test database has invalid second template hash, got %q, want %q", test2.TemplateHash, hash) 139 | } 140 | 141 | if test2.ID == test.ID { 142 | t.Error("received same test database a second time without returning") 143 | } 144 | 145 | db2, err := sql.Open("postgres", test2.Config.ConnectionString()) 146 | if err != nil { 147 | t.Fatalf("failed to open second test database connection: %v", err) 148 | } 149 | defer db2.Close() 150 | 151 | if err := db2.Ping(); err != nil { 152 | t.Fatalf("failed to ping second test database connection: %v", err) 153 | } 154 | } 155 | 156 | func TestClientReturnTestDatabase(t *testing.T) { 157 | ctx := context.Background() 158 | 159 | c, err := DefaultClientFromEnv() 160 | if err != nil { 161 | t.Fatalf("failed to create client: %v", err) 162 | } 163 | 164 | if err := c.ResetAllTracking(ctx); err != nil { 165 | t.Fatalf("failed to reset all test pool tracking: %v", err) 166 | } 167 | 168 | hash := "hashinghash4" 169 | 170 | if _, err := c.InitializeTemplate(ctx, hash); err != nil { 171 | t.Fatalf("failed to initialize template: %v", err) 172 | } 173 | 174 | if err := c.FinalizeTemplate(ctx, hash); err != nil { 175 | t.Fatalf("failed to finalize template: %v", err) 176 | } 177 | 178 | test, err := c.GetTestDatabase(ctx, hash) 179 | if err != nil { 180 | t.Fatalf("failed to get test database: %v", err) 181 | } 182 | 183 | if test.TemplateHash != hash { 184 | t.Errorf("test database has invalid template hash, got %q, want %q", test.TemplateHash, hash) 185 | } 186 | 187 | if err := c.ReturnTestDatabase(ctx, hash, test.ID); err != nil { 188 | t.Fatalf("failed to return test database: %v", err) 189 | } 190 | 191 | test2, err := c.GetTestDatabase(ctx, hash) 192 | if err != nil { 193 | t.Fatalf("failed to get second test database: %v", err) 194 | } 195 | 196 | if test2.TemplateHash != hash { 197 | t.Errorf("test database has invalid second template hash, got %q, want %q", test2.TemplateHash, hash) 198 | } 199 | 200 | if test2.ID != test.ID { 201 | t.Errorf("received invalid test database, want %d, got %d", test.ID, test2.ID) 202 | } 203 | } 204 | 205 | func populateTemplateDB(ctx context.Context, db *sql.DB) error { 206 | if _, err := db.ExecContext(ctx, ` 207 | CREATE EXTENSION "uuid-ossp"; 208 | CREATE TABLE pilots ( 209 | id uuid NOT NULL DEFAULT uuid_generate_v4(), 210 | "name" text NOT NULL, 211 | created_at timestamptz NOT NULL, 212 | updated_at timestamptz NULL, 213 | CONSTRAINT pilot_pkey PRIMARY KEY (id) 214 | ); 215 | CREATE TABLE jets ( 216 | id uuid NOT NULL DEFAULT uuid_generate_v4(), 217 | pilot_id uuid NOT NULL, 218 | age int4 NOT NULL, 219 | "name" text NOT NULL, 220 | color text NOT NULL, 221 | created_at timestamptz NOT NULL, 222 | updated_at timestamptz NULL, 223 | CONSTRAINT jet_pkey PRIMARY KEY (id) 224 | ); 225 | ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); 226 | `); err != nil { 227 | return err 228 | } 229 | 230 | tx, err := db.BeginTx(ctx, nil) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | if _, err := tx.ExecContext(ctx, ` 236 | INSERT INTO pilots (id, "name", created_at, updated_at) VALUES ('744a1a87-5ef7-4309-8814-0f1054751156', 'Mario', '2020-03-23 09:44:00.548', '2020-03-23 09:44:00.548'); 237 | INSERT INTO pilots (id, "name", created_at, updated_at) VALUES ('20d9d155-2e95-49a2-8889-2ae975a8617e', 'Nick', '2020-03-23 09:44:00.548', '2020-03-23 09:44:00.548'); 238 | INSERT INTO jets (id, pilot_id, age, "name", color, created_at, updated_at) VALUES ('67d9d0c7-34e5-48b0-9c7d-c6344995353c', '744a1a87-5ef7-4309-8814-0f1054751156', 26, 'F-14B', 'grey', '2020-03-23 09:44:00.000', '2020-03-23 09:44:00.000'); 239 | INSERT INTO jets (id, pilot_id, age, "name", color, created_at, updated_at) VALUES ('facaf791-21b4-401a-bbac-67079ae4921f', '20d9d155-2e95-49a2-8889-2ae975a8617e', 27, 'F-14B', 'grey/red', '2020-03-23 09:44:00.000', '2020-03-23 09:44:00.000'); 240 | `); err != nil { 241 | return err 242 | } 243 | 244 | if err := tx.Commit(); err != nil { 245 | return err 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func TestSetupTemplateWithDBClient(t *testing.T) { 252 | ctx := context.Background() 253 | 254 | c, err := DefaultClientFromEnv() 255 | if err != nil { 256 | t.Fatalf("failed to create client: %v", err) 257 | } 258 | 259 | if err := c.ResetAllTracking(ctx); err != nil { 260 | t.Fatalf("failed to reset all test pool tracking: %v", err) 261 | } 262 | 263 | hash := "hashinghash5" 264 | 265 | if err := c.SetupTemplateWithDBClient(ctx, hash, func(db *sql.DB) error { 266 | 267 | // setup code 268 | if err := populateTemplateDB(ctx, db); err != nil { 269 | t.Fatalf("failed to populate template db: %v", err) 270 | } 271 | 272 | return err 273 | }); err != nil { 274 | t.Fatalf("Failed to setup template database for hash %q: %v", hash, err) 275 | } 276 | } 277 | 278 | func getTestDB(wg *sync.WaitGroup, errs chan<- error, c *Client, hash string) { 279 | defer wg.Done() 280 | 281 | _, err := c.GetTestDatabase(context.Background(), hash) 282 | if err != nil { 283 | errs <- err 284 | return 285 | } 286 | 287 | errs <- nil 288 | } 289 | 290 | func TestSetupTemplateWithDBClientFailingSetupCodeAndReinitialize(t *testing.T) { 291 | ctx := context.Background() 292 | 293 | c, err := DefaultClientFromEnv() 294 | if err != nil { 295 | t.Fatalf("failed to create client: %v", err) 296 | } 297 | 298 | if err := c.ResetAllTracking(ctx); err != nil { 299 | t.Fatalf("failed to reset all test pool tracking: %v", err) 300 | } 301 | 302 | hash := "hashinghash5" 303 | 304 | testDBWhileInitializeCount := 5 305 | testDBPreDiscardCount := 5 306 | testDBAfterDiscardCount := 5 307 | 308 | allTestDbCount := testDBWhileInitializeCount + testDBPreDiscardCount + testDBAfterDiscardCount 309 | 310 | var errs = make(chan error, allTestDbCount) 311 | var wg sync.WaitGroup 312 | 313 | if err := c.SetupTemplateWithDBClient(ctx, hash, func(db *sql.DB) error { 314 | 315 | // setup code 316 | if err := populateTemplateDB(ctx, db); err != nil { 317 | t.Fatalf("failed to populate template db: %v", err) 318 | } 319 | 320 | // some other participents are already connecting... 321 | wg.Add(testDBWhileInitializeCount) 322 | 323 | for i := 0; i < testDBWhileInitializeCount; i++ { 324 | go getTestDB(&wg, errs, c, hash) 325 | } 326 | 327 | // but then we throw an error during our test setup! 328 | err = errors.New("FAILED ERR DURING INITIALIZE") 329 | 330 | return err 331 | }); err == nil { 332 | t.Fatalf("we expected this to error!!") 333 | } 334 | 335 | // some other participents are still want to be part of this... 336 | wg.Add(testDBPreDiscardCount) 337 | 338 | for i := 0; i < testDBPreDiscardCount; i++ { 339 | go getTestDB(&wg, errs, c, hash) 340 | } 341 | 342 | // SIGNAL DISCARD! 343 | err = c.DiscardTemplate(ctx, hash) 344 | 345 | if err != nil { 346 | t.Fatalf("failed to discard template database after error during initialize: %v", err) 347 | } 348 | 349 | // finalize template should now no longer work 350 | err = c.FinalizeTemplate(ctx, hash) 351 | 352 | if err == nil { 353 | t.Fatalf("finalize template should not work after a successful discard!: %v", err) 354 | } 355 | 356 | // haven't learned other participents are still want to be part of this... 357 | wg.Add(testDBAfterDiscardCount) 358 | 359 | for i := 0; i < testDBAfterDiscardCount; i++ { 360 | go getTestDB(&wg, errs, c, hash) 361 | } 362 | 363 | // wait for all the errors with getTestDB to arrive... 364 | wg.Wait() 365 | 366 | var results = make([]error, 0, allTestDbCount) 367 | for i := 0; i < allTestDbCount; i++ { 368 | results = append(results, <-errs) 369 | } 370 | 371 | close(errs) 372 | 373 | // check all getTestDatabase clients also errored out... 374 | success := 0 375 | errored := 0 376 | for _, err := range results { 377 | if err == nil { 378 | success++ 379 | } else { 380 | // fmt.Println(err) 381 | errored++ 382 | } 383 | } 384 | 385 | if errored != allTestDbCount { 386 | t.Errorf("invalid number of errored retrievals, got %d, want %d", errored, allTestDbCount) 387 | } 388 | 389 | if success != 0 { 390 | t.Errorf("invalid number of successful retrievals, got %d, want %d", success, 0) 391 | } 392 | 393 | // then test a successful reinitialize... 394 | if err := c.SetupTemplateWithDBClient(ctx, hash, func(db *sql.DB) error { 395 | 396 | errr := populateTemplateDB(ctx, db) 397 | 398 | // setup code 399 | if errr != nil { 400 | t.Fatalf("failed to repopulate template db: %v", errr) 401 | } 402 | 403 | return errr 404 | }); err != nil { 405 | t.Fatalf("Failed to resetup template database for hash %q: %v", hash, err) 406 | } 407 | } 408 | --------------------------------------------------------------------------------