├── .gitignore ├── Dockerfile.centos7 ├── LICENSE ├── Makefile ├── README.asciidoc ├── adapter ├── adapter.go ├── audit.go ├── decorate.go └── mock.go ├── cli ├── cli.go ├── flags.go ├── health.go ├── node.go ├── start.go ├── stats.go ├── stop.go ├── util.go └── version.go ├── common └── common.go ├── config └── config.go ├── connect ├── auth.go ├── connect.go └── ssl.go ├── docs ├── README.md ├── bootstrap.js ├── build-docs.sh ├── crunchy-proxy-ssl-guide.asciidoc ├── crunchy-proxy-user-guide.asciidoc ├── crunchy_logo.png ├── footer.html ├── proxy-diagram.png ├── proxy-diagram.svg └── proxy-golang.png ├── examples ├── config.yaml └── example-config-ssl.yaml ├── glide.lock ├── glide.yaml ├── main.go ├── pool └── pool.go ├── protocol ├── auth.go ├── errcodes.txt ├── error.go ├── errorcodes.go ├── message.go ├── protocol.go └── startup.go ├── proxy ├── annotation.go ├── parse.go └── proxy.go ├── scripts ├── .gitignore ├── certs │ ├── README │ ├── client │ │ ├── ca.crt │ │ ├── client.crt │ │ └── client.key │ ├── generate-client-cert.sh │ ├── generate-ssl.sh │ └── server │ │ ├── ca.crt │ │ ├── server.crt │ │ └── server.key ├── docker │ ├── config.yaml │ ├── master-config │ │ ├── pg_hba.conf │ │ └── postgresql.conf │ ├── replica-config │ │ ├── pg_hba.conf │ │ └── postgresql.conf │ ├── run-cluster-config.sh │ ├── run-cluster.sh │ ├── run-docker.sh │ └── run-pgpool.sh ├── install-deps.sh └── vagrant │ ├── README │ ├── Vagrantfile │ ├── ansible │ ├── files │ │ ├── certs │ │ └── pgpool.config │ ├── handlers │ │ └── main.yml │ ├── master.yml │ ├── replica.yml │ ├── tasks │ │ ├── common-tasks.yml │ │ ├── pg-common-tasks.yml │ │ ├── pg-configure-tasks.yml │ │ ├── pg-intialize-tasks.yml │ │ └── pg-ssl-tasks.yml │ ├── test.yml │ └── vars │ │ ├── master-vars.yml │ │ ├── pg-common-vars.yml │ │ └── replica-vars.yml │ └── initialize-replica.sh ├── server ├── admin.go ├── proxy.go ├── server.go └── serverpb │ ├── admin.pb.go │ └── admin.proto ├── testclient ├── run-test-pgpool.sh ├── run-test-proxy.sh └── testclient.go ├── tests ├── admin_test.go ├── audit_test.go ├── common.go ├── healthcheck_test.go ├── main_test.go ├── master-only │ └── config.json ├── master-replica │ └── config.json ├── overhead_test.go ├── pgbench │ ├── README.md │ ├── concurrency-test.sql │ ├── config.yaml │ ├── init-tests.sh │ ├── pgpass │ ├── run-concurrency-test.sh │ ├── run-simple-load-test.sh │ └── simple-load-test.sql ├── psql │ └── txn_test.sql ├── retry_test.go ├── select_noanno_test.go ├── select_test.go └── txn_test.go └── util ├── grpcutil ├── grpcutil.go └── log.go └── log └── logger.go /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore build/dependency related directories 2 | vendor 3 | build 4 | dist 5 | docs/pdf 6 | 7 | # ignore test/dev environment related directories 8 | .docker 9 | .vagrant 10 | 11 | *.swp 12 | *.retry 13 | -------------------------------------------------------------------------------- /Dockerfile.centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | LABEL Release="1.0.0-beta" Vendor="Crunchy Data Solutions" 4 | 5 | RUN yum -y install openssh-clients hostname && yum clean all -y 6 | 7 | ADD build/crunchy-proxy /usr/bin 8 | 9 | VOLUME /config 10 | 11 | EXPOSE 5432 12 | 13 | USER daemon 14 | 15 | CMD ["crunchy-proxy","start", "--config=/config/config.yaml" ] 16 | 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Crunchy Data Solutions, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations under 13 | # the License. 14 | 15 | .PHONY: all build clean clean-docs docs docker docker-push resolve install release run default 16 | 17 | all: clean resolve build 18 | 19 | RELEASE_VERSION := 1.0.0-beta 20 | PROJECT_DIR := $(shell pwd) 21 | BUILD_DIR := $(PROJECT_DIR)/build 22 | DIST_DIR := $(PROJECT_DIR)/dist 23 | VENDOR_DIR := $(PROJECT_DIR)/vendor 24 | DOCS_DIR := $(PROJECT_DIR)/docs 25 | 26 | BUILD_TARGET := $(PROJECT_DIR)/main.go 27 | 28 | CRUNCHY_PROXY := crunchy-proxy 29 | RELEASE_ARCHIVE := $(DIST_DIR)/crunchyproxy-$(RELEASE_VERSION).tar.gz 30 | 31 | 32 | clean: 33 | @echo "Cleaning project..." 34 | @rm -rf $(DIST_DIR) 35 | @rm -rf $(VENDOR_DIR) 36 | @go clean -i 37 | 38 | resolve: 39 | @echo "Resolving depenencies..." 40 | @glide up 41 | 42 | build: 43 | @echo "Building project..." 44 | @go build -i -o $(BUILD_DIR)/$(CRUNCHY_PROXY) 45 | 46 | install: 47 | @go install 48 | 49 | clean-docs: 50 | @rm -rf $(DOCS_DIR)/pdf 51 | 52 | docs: 53 | @mkdir -p $(DOCS_DIR)/pdf 54 | @cd docs && ./build-docs.sh 55 | 56 | release: clean resolve build 57 | @echo "Creating $(RELEASE_VERSION) release archive..." 58 | @mkdir -p $(DIST_DIR) 59 | @tar czf $(RELEASE_ARCHIVE) -C $(BUILD_DIR) $(CRUNCHY_PROXY) 60 | @echo "Created: $(RELEASE_ARCHIVE)" 61 | 62 | run: 63 | @go run main.go --config=./examples/config.yaml 64 | 65 | docker: 66 | docker build -t crunchy-proxy -f Dockerfile.centos7 . 67 | docker tag crunchy-proxy crunchydata/crunchy-proxy:centos7-$(RELEASE_VERSION) 68 | 69 | docker-push: 70 | docker push crunchydata/crunchy-proxy:centos7-$(RELEASE_VERSION) 71 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | = Installation Guide - Crunchy Proxy 2 | Crunchy Data Solutions, Inc. 3 | v0.0.1, {docdate} 4 | image::docs/crunchy_logo.png?raw=true[] 5 | 6 | == Project 7 | 8 | *crunchy-proxy* is a minimal PostgreSQL-aware proxy used to handle PostgreSQL application requests. The diagram below depicts a PostgreSQL client application connecting to the *crunchy-proxy*, which appears to the PostgreSQL application as any other PostgreSQL database connection. The *crunchy-proxy* accepts the inbound client requests, and routes them to an appropriate PostgreSQL cluster member (as described in more detail below). 9 | 10 | image::docs/proxy-diagram.png?raw=true["scaledwidth="50%"] 11 | 12 | To the PostgreSQL server, the *crunchy-proxy* is transparent and appears as any other 13 | PostgreSQL client would appear. As described in more detail below, the *crunchy-proxy* currently provides the following capabilities: 14 | 15 | * ability to route messages based on the SQL command type - writes are 16 | sent to a master and reads are sent to replicas in a round-robin fashion 17 | * configuration via JSON file 18 | * PostgreSQL healthcheck 19 | * ability to route inbound client messages based on the health of PostgreSQL 20 | * REST administrative interface 21 | * ability to publish healthcheck events to consumers outside of the proxy 22 | * ability to load balance SQL statements across multiple replica backends 23 | * connection pooling 24 | * support for certificate and SSL based authentication 25 | 26 | As described under "Approach" below, implementation of the *crunchy-proxy* requires that (1) a user has access to their application's SQL and (2) a user has the ability to add basic annotations to their SQL statements. 27 | 28 | == Approach 29 | 30 | One of the key features of the *crunchy-proxy* is the ability to route different SQL statements to a PostgreSQL cluster members based on certain pre-defined rules. This capability enables end-user applications to see a PostgreSQL cluster as a single connection. In order to support this capability, it is necessary that *crunchy-proxy* have knowledge of the SQL statements that a PostgreSQL application sends to PostgreSQL. 31 | 32 | One approach to addressing the requirement that a proxy be "SQL aware" (e.g. link:http://www.pgpool.net/mediawiki/index.php/Main_Page[pgpool]) is to first parse each SQL statement looking for grammar that would imply a *write* or *update* and to then send those SQL statements to the cluster master. SQL statements that are determined to be *read-only* in nature are deemed safe to send to the cluster replica members. Remember, in PostgreSQL cluster replica members are only able to process SQL that is *read-only* in nature. 33 | 34 | *crunchy-proxy* takes a different approach to SQL routing. Instead of attempting to parse the entire SQL grammer of each SQL statement, it looks for a simple annotation, supplied by the end-user within their SQL statement, to determine the routing destination. 35 | 36 | For example, the following SQL statement is annotated to indicate it 37 | is a *read-only* SQL statement: 38 | .... 39 | /* read */ select now(); 40 | .... 41 | 42 | Examples of a write SQL statement include the following: 43 | .... 44 | create table boofar (id int); 45 | drop table boofar (id int); 46 | .... 47 | 48 | If a SQL statement does not include an annotation, the the statement is deemed a *write* and thus sent to the master cluster member. 49 | 50 | By parsing only the annotation, *crunchy-proxy* simplifies the complexity associated with determining whether a SQL statement is a *write* or *read* and thus to which member (master or replica) of a PostgreSQL cluser to send a SQL statement. 51 | 52 | In taking this approach, *crunchy-proxy* has assumed (1) a user has access to their application's SQL and (2) a user has the ability to add the annotation in their SQL statements. If they do, then they can use the *crunchy-proxy* for SQL routing. 53 | 54 | Of course these assumptions introduce certain limitations on the *crunchy-proxy*. Nonetheless, it was determined that these assumptions will not be unduly limiting in the usability of the *crunchy-proxy* and that the resulting limiations are justified by the benefits of (1) reduction in complexity associated with SQL parsing implementation, (2) increase in proxy throughput and (3) improved routing accuracy of the SQL parsing. 55 | 56 | == PostgreSQL Wire Protocol 57 | 58 | *crunchy-proxy* operates at the PostgreSQL wire protocol (network) layer to understand PostgreSQL client authentication requests and SQL statements passed by a client to a PostgreSQL backend. 59 | 60 | As *crunchy-proxy* uses annotations to route messages to the backend, the proxy primarily examines SQL statements for proxy-specific annotations and does very little processing of the messages sent between a client and an actual backend. 61 | 62 | Its important to note that the proxy does not implement all features of libpq or provide an application interface similar to a JDBC driver or other language driver. 63 | 64 | The following resources are useful in understanding the PostgreSQL wire protocol: 65 | 66 | * link:https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf[https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf] 67 | * link:https://www.postgresql.org/docs/current/static/protocol.html[https://www.postgresql.org/docs/current/static/protocol.html] 68 | * link:https://github.com/lib/pq[https://github.com/lib/pq] 69 | 70 | In the future, by working at the wire protocol level, *crunchy-proxy* can implement a variety of features important for high speed proxy handling and for supporting PostgreSQL features. 71 | 72 | == Execution 73 | 74 | The proxy is a golang binary, you execute it as follows: 75 | .... 76 | $> crunchy-proxy start --config=config.yaml 77 | .... 78 | 79 | To run the proxy at different logging output levels: 80 | 81 | .... 82 | $> crunchy-proxy start --config=config.yaml --log-level= 83 | .... 84 | 85 | Where __ is one of the following: 86 | 87 | * *debug* 88 | * *info* 89 | * *error* 90 | * *fatal* 91 | 92 | Detailed documentation including configuration file format and 93 | developer information is 94 | found in the link:docs/crunchy-proxy-user-guide.asciidoc[User Guide] 95 | 96 | For Docker users, you can run the proxy using the 97 | link:bin/run-docker.sh[run-docker.sh] script. 98 | 99 | == Feedback 100 | 101 | If you find a bug, or want to provide feedback on the design and features 102 | feel free to create a github issue. 103 | 104 | == Legal Notices 105 | 106 | Copyright © 2017 Crunchy Data Solutions, Inc. 107 | 108 | CRUNCHY DATA SOLUTIONS, INC. PROVIDES THIS GUIDE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF NON INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 109 | 110 | Crunchy, Crunchy Data Solutions, Inc. and the Crunchy Hippo Logo are trademarks of Crunchy Data Solutions, Inc. 111 | 112 | -------------------------------------------------------------------------------- /adapter/adapter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package adapter 15 | 16 | type Adapter interface { 17 | Do([]byte, int) error 18 | } 19 | 20 | type AdapterFunc func([]byte, int) error 21 | 22 | func (f AdapterFunc) Do(r []byte, i int) error { 23 | return f(r, i) 24 | } 25 | -------------------------------------------------------------------------------- /adapter/audit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package adapter 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "os" 20 | "time" 21 | ) 22 | 23 | var file *os.File 24 | 25 | func createFile(metadata map[string]interface{}, l *log.Logger) { 26 | 27 | var err error 28 | 29 | var filepath = metadata["filepath"].(string) 30 | if filepath == "" { 31 | filepath = "/tmp/audit.log" 32 | } 33 | l.Println("audit adapter using filepath of " + filepath) 34 | 35 | file, err = os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 36 | if err != nil { 37 | log.Println(err.Error()) 38 | } 39 | } 40 | 41 | // Audit will create a adapter decorator with auditing concerns. 42 | func Audit(metadata map[string]interface{}, l *log.Logger) Decorator { 43 | return func(c Adapter) Adapter { 44 | return AdapterFunc(func(r []byte, i int) error { 45 | now := time.Now() 46 | if file == nil { 47 | createFile(metadata, l) 48 | } 49 | file.WriteString(fmt.Sprintf("%s msg len=%d\n", now.String(), i)) 50 | file.Sync() 51 | return c.Do(r, i) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /adapter/decorate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package adapter 15 | 16 | // We define what a decorator to our adapter will look like 17 | type Decorator func(Adapter) Adapter 18 | 19 | // Decorate will decorate a adapter with a slice of passed decorators 20 | func Decorate(c Adapter, ds ...Decorator) Adapter { 21 | decorated := c 22 | for _, decorate := range ds { 23 | decorated = decorate(decorated) 24 | } 25 | return decorated 26 | } 27 | 28 | func ThisDecorate(c Adapter, ds []Decorator) Adapter { 29 | decorated := c 30 | for _, decorate := range ds { 31 | decorated = decorate(decorated) 32 | } 33 | return decorated 34 | } 35 | -------------------------------------------------------------------------------- /adapter/mock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package adapter 15 | 16 | type MockAdapter struct{} 17 | 18 | func (mc MockAdapter) Do(r []byte, i int) error { 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | func Main() { 25 | if err := Run(os.Args[1:]); err != nil { 26 | fmt.Fprintf(os.Stderr, "Failed running %q\n", os.Args[1]) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | var crunchyproxyCmd = &cobra.Command{ 32 | Use: "crunchy-proxy", 33 | Short: "A simple connection pool based SQL routing proxy", 34 | SilenceUsage: true, 35 | } 36 | 37 | func init() { 38 | cobra.EnableCommandSorting = false 39 | 40 | crunchyproxyCmd.AddCommand( 41 | startCmd, 42 | stopCmd, 43 | nodeCmd, 44 | statsCmd, 45 | healthCmd, 46 | versionCmd, 47 | ) 48 | } 49 | 50 | func Run(args []string) error { 51 | crunchyproxyCmd.SetArgs(args) 52 | return crunchyproxyCmd.Execute() 53 | } 54 | -------------------------------------------------------------------------------- /cli/flags.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "github.com/spf13/pflag" 19 | ) 20 | 21 | var host string 22 | var port string 23 | 24 | var format string 25 | 26 | type flagInfoString struct { 27 | Name string 28 | Shorthand string 29 | Description string 30 | Default string 31 | } 32 | 33 | type flagInfoBool struct { 34 | Name string 35 | Shorthand string 36 | Description string 37 | Default bool 38 | } 39 | 40 | var ( 41 | FlagAdminHost = flagInfoString{ 42 | Name: "host", 43 | Description: "proxy admin server address", 44 | Default: "localhost", 45 | } 46 | 47 | FlagAdminPort = flagInfoString{ 48 | Name: "port", 49 | Description: "proxy admin server port", 50 | Default: "8000", 51 | } 52 | 53 | FlagOutputFormat = flagInfoString{ 54 | Name: "format", 55 | Description: "the output format", 56 | Default: "plain", 57 | } 58 | 59 | FlagConfigPath = flagInfoString{ 60 | Name: "config", 61 | Shorthand: "c", 62 | Description: "path to configuration file", 63 | } 64 | 65 | FlagLogLevel = flagInfoString{ 66 | Name: "log-level", 67 | Description: "logging level", 68 | Default: "info", 69 | } 70 | 71 | FlagBackground = flagInfoBool{ 72 | Name: "background", 73 | Description: "run process in background", 74 | Default: false, 75 | } 76 | ) 77 | 78 | func stringFlag(f *pflag.FlagSet, valPtr *string, flagInfo flagInfoString) { 79 | f.StringVarP(valPtr, 80 | flagInfo.Name, 81 | flagInfo.Shorthand, 82 | flagInfo.Default, 83 | flagInfo.Description) 84 | } 85 | 86 | func boolFlag(f *pflag.FlagSet, valPtr *bool, flagInfo flagInfoBool) { 87 | f.BoolVarP(valPtr, 88 | flagInfo.Name, 89 | flagInfo.Shorthand, 90 | flagInfo.Default, 91 | flagInfo.Description) 92 | } 93 | -------------------------------------------------------------------------------- /cli/health.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | "golang.org/x/net/context" 23 | "google.golang.org/grpc" 24 | 25 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 26 | ) 27 | 28 | var healthCmd = &cobra.Command{ 29 | Use: "health [options]", 30 | Short: "show health check information for configured nodes", 31 | RunE: runHealth, 32 | } 33 | 34 | func init() { 35 | flags := healthCmd.Flags() 36 | 37 | stringFlag(flags, &host, FlagAdminHost) 38 | stringFlag(flags, &port, FlagAdminPort) 39 | stringFlag(flags, &format, FlagOutputFormat) 40 | } 41 | 42 | func runHealth(cmd *cobra.Command, args []string) error { 43 | var result string 44 | address := fmt.Sprintf("%s:%s", host, port) 45 | 46 | dialOptions := []grpc.DialOption{ 47 | grpc.WithDialer(adminServerDialer), 48 | grpc.WithInsecure(), 49 | } 50 | 51 | conn, err := grpc.Dial(address, dialOptions...) 52 | 53 | if err != nil { 54 | fmt.Println(err) 55 | } 56 | 57 | client := pb.NewAdminClient(conn) 58 | 59 | ctx, cancel := context.WithCancel(context.Background()) 60 | defer cancel() 61 | 62 | response, err := client.Health(ctx, &pb.HealthRequest{}) 63 | conn.Close() 64 | 65 | if err != nil { 66 | fmt.Printf("Error: %s", err.Error()) 67 | return err 68 | } 69 | 70 | switch format { 71 | case "json": 72 | j, _ := json.Marshal(response.GetHealth()) 73 | result = string(j) 74 | case "plain": 75 | result = formatPlain(response.GetHealth()) 76 | default: 77 | result = fmt.Sprintf("Error: Unsupported format '%s'", format) 78 | } 79 | 80 | fmt.Println(result) 81 | 82 | return nil 83 | } 84 | 85 | func formatPlain(hc map[string]bool) string { 86 | var result string 87 | 88 | for name, healthy := range hc { 89 | result += fmt.Sprintf("* %s: %t\n", name, healthy) 90 | } 91 | 92 | return result 93 | } 94 | -------------------------------------------------------------------------------- /cli/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | "golang.org/x/net/context" 23 | "google.golang.org/grpc" 24 | 25 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 26 | ) 27 | 28 | var nodeCmd = &cobra.Command{ 29 | Use: "node", 30 | Short: "show information about configured nodes", 31 | RunE: runNode, 32 | } 33 | 34 | var nodeListCmd = &cobra.Command{} 35 | 36 | func init() { 37 | flags := nodeCmd.Flags() 38 | 39 | stringFlag(flags, &host, FlagAdminHost) 40 | stringFlag(flags, &port, FlagAdminPort) 41 | stringFlag(flags, &format, FlagOutputFormat) 42 | } 43 | 44 | func runNode(cmd *cobra.Command, args []string) error { 45 | address := fmt.Sprintf("%s:%s", host, port) 46 | 47 | dialOptions := []grpc.DialOption{ 48 | grpc.WithDialer(adminServerDialer), 49 | grpc.WithInsecure(), 50 | } 51 | 52 | conn, err := grpc.Dial(address, dialOptions...) 53 | 54 | if err != nil { 55 | fmt.Println(err.Error()) 56 | } 57 | 58 | defer conn.Close() 59 | 60 | c := pb.NewAdminClient(conn) 61 | 62 | response, err := c.Nodes(context.Background(), &pb.NodeRequest{}) 63 | 64 | if err != nil { 65 | fmt.Println(err.Error()) 66 | } 67 | 68 | var result string 69 | nodes := response.GetNodes() 70 | 71 | switch format { 72 | case "json": 73 | j, _ := json.Marshal(nodes) 74 | result = string(j) 75 | case "plain": 76 | for name, node := range nodes { 77 | result += fmt.Sprintf("* %s - %s\n", name, node) 78 | } 79 | default: 80 | result = fmt.Sprintf("Error: Unsupported format - '%s'", format) 81 | } 82 | 83 | fmt.Println(result) 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /cli/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "os" 19 | "os/exec" 20 | "strings" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/crunchydata/crunchy-proxy/config" 25 | "github.com/crunchydata/crunchy-proxy/server" 26 | "github.com/crunchydata/crunchy-proxy/util/log" 27 | ) 28 | 29 | var background bool 30 | var configPath string 31 | var logLevel string 32 | 33 | var startCmd = &cobra.Command{ 34 | Use: "start", 35 | Short: "start a proxy instance", 36 | Long: "", 37 | Example: "", 38 | RunE: runStart, 39 | } 40 | 41 | func init() { 42 | flags := startCmd.Flags() 43 | boolFlag(flags, &background, FlagBackground) 44 | stringFlag(flags, &configPath, FlagConfigPath) 45 | stringFlag(flags, &logLevel, FlagLogLevel) 46 | } 47 | 48 | func runStart(cmd *cobra.Command, args []string) error { 49 | if background { 50 | args = make([]string, 0, len(os.Args)) 51 | 52 | for _, arg := range os.Args { 53 | if strings.HasPrefix(arg, "--background") { 54 | continue 55 | } 56 | args = append(args, arg) 57 | } 58 | 59 | cmd := exec.Command(args[0], args[1:]...) 60 | cmd.Stdout = os.Stdout 61 | cmd.Stderr = os.Stderr 62 | 63 | return cmd.Start() 64 | } 65 | 66 | log.SetLevel(logLevel) 67 | 68 | if configPath != "" { 69 | config.SetConfigPath(configPath) 70 | } 71 | 72 | config.ReadConfig() 73 | 74 | s := server.NewServer() 75 | 76 | s.Start() 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /cli/stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | "golang.org/x/net/context" 23 | "google.golang.org/grpc" 24 | 25 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 26 | ) 27 | 28 | var statsCmd = &cobra.Command{ 29 | Use: "stats [options]", 30 | Short: "show query statistics for configured nodes", 31 | RunE: runStats, 32 | } 33 | 34 | func init() { 35 | flags := statsCmd.Flags() 36 | 37 | stringFlag(flags, &host, FlagAdminHost) 38 | stringFlag(flags, &port, FlagAdminPort) 39 | stringFlag(flags, &format, FlagOutputFormat) 40 | } 41 | 42 | func runStats(cmd *cobra.Command, args []string) error { 43 | address := fmt.Sprintf("%s:%s", host, port) 44 | 45 | dialOptions := []grpc.DialOption{ 46 | grpc.WithDialer(adminServerDialer), 47 | grpc.WithInsecure(), 48 | } 49 | 50 | conn, err := grpc.Dial(address, dialOptions...) 51 | 52 | if err != nil { 53 | fmt.Println(err) 54 | } 55 | 56 | defer conn.Close() 57 | 58 | c := pb.NewAdminClient(conn) 59 | 60 | response, err := c.Statistics(context.Background(), &pb.StatisticsRequest{}) 61 | 62 | if err != nil { 63 | fmt.Println(err) 64 | } 65 | 66 | var result string 67 | queries := response.GetQueries() 68 | 69 | switch format { 70 | case "json": 71 | j, _ := json.Marshal(queries) 72 | result = string(j) 73 | case "plain": 74 | for name, query := range queries { 75 | result += fmt.Sprintf("* %s - %d\n", name, query) 76 | } 77 | default: 78 | result = fmt.Sprintf("Error: Unsupported format - '%s'", format) 79 | } 80 | 81 | fmt.Println(result) 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /cli/stop.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | "golang.org/x/net/context" 22 | "google.golang.org/grpc" 23 | 24 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 25 | ) 26 | 27 | var stopCmd = &cobra.Command{ 28 | Use: "stop", 29 | Short: "stop a running instance of a proxy", 30 | RunE: runStop, 31 | } 32 | 33 | func init() { 34 | flags := stopCmd.Flags() 35 | 36 | stringFlag(flags, &host, FlagAdminHost) 37 | stringFlag(flags, &port, FlagAdminPort) 38 | } 39 | 40 | func runStop(cmd *cobra.Command, args []string) error { 41 | address := fmt.Sprintf("%s:%s", host, port) 42 | 43 | dialOptions := []grpc.DialOption{ 44 | grpc.WithDialer(adminServerDialer), 45 | grpc.WithInsecure(), 46 | } 47 | 48 | conn, err := grpc.Dial(address, dialOptions...) 49 | 50 | if err != nil { 51 | fmt.Println(err) 52 | } 53 | 54 | client := pb.NewAdminClient(conn) 55 | 56 | ctx, cancel := context.WithCancel(context.Background()) 57 | defer cancel() 58 | 59 | client.Shutdown(ctx, &pb.ShutdownRequest{}) 60 | conn.Close() 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /cli/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "os" 21 | "time" 22 | ) 23 | 24 | // Custom dialer for use with grpc dialer. 25 | func adminServerDialer(address string, timeout time.Duration) (net.Conn, error) { 26 | conn, err := net.DialTimeout("tcp", address, timeout) 27 | 28 | if err != nil { 29 | fmt.Printf("Could not connect - %s\n", err.Error()) 30 | os.Exit(1) 31 | } 32 | 33 | return conn, err 34 | } 35 | -------------------------------------------------------------------------------- /cli/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/spf13/cobra" 21 | "golang.org/x/net/context" 22 | "google.golang.org/grpc" 23 | 24 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 25 | ) 26 | 27 | var versionCmd = &cobra.Command{ 28 | Use: "version", 29 | Short: "show version information for instance of proxy", 30 | Long: "", 31 | Example: "", 32 | RunE: runVersion, 33 | } 34 | 35 | func init() { 36 | flags := versionCmd.Flags() 37 | 38 | stringFlag(flags, &host, FlagAdminHost) 39 | stringFlag(flags, &port, FlagAdminPort) 40 | } 41 | 42 | func runVersion(cmd *cobra.Command, args []string) error { 43 | address := fmt.Sprintf("%s:%s", host, port) 44 | 45 | dialOptions := []grpc.DialOption{ 46 | grpc.WithDialer(adminServerDialer), 47 | grpc.WithInsecure(), 48 | } 49 | 50 | conn, err := grpc.Dial(address, dialOptions...) 51 | 52 | if err != nil { 53 | fmt.Println(err) 54 | } 55 | 56 | defer conn.Close() 57 | 58 | c := pb.NewAdminClient(conn) 59 | 60 | response, err := c.Version(context.Background(), &pb.VersionRequest{}) 61 | 62 | if err != nil { 63 | fmt.Println(err) 64 | } 65 | 66 | fmt.Println(response.Version) 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "net" 19 | ) 20 | 21 | const ( 22 | NODE_ROLE_MASTER string = "master" 23 | NODE_ROLE_REPLICA string = "replica" 24 | ) 25 | 26 | type Node struct { 27 | HostPort string `mapstructure:"hostport"` //remote host:port 28 | Role string `mapstructure:"role"` 29 | Metadata map[string]string `mapstructure:"metadata"` 30 | Healthy bool `mapstructure:"-"` 31 | } 32 | 33 | type Pool struct { 34 | Channel chan int `mapstructure:"-"` 35 | Connections []net.Conn `mapstructure:"-"` 36 | } 37 | 38 | type SSLConfig struct { 39 | Enable bool `mapstructure:"enable"` 40 | SSLMode string `mapstructure:"sslmode"` 41 | SSLCert string `mapstructure:"sslcert,omitempty"` 42 | SSLKey string `mapstructure:"sslkey,omitempty"` 43 | SSLRootCA string `mapstructure:"sslrootca,omitempty"` 44 | SSLServerCert string `mapstructure:"sslservercert,omitempty"` 45 | SSLServerKey string `mapstructure:"sslserverkey,omitempty"` 46 | SSLServerCA string `mapstructure:"sslserverca,omitempty"` 47 | } 48 | 49 | type Credentials struct { 50 | Username string `mapstructure:"username"` 51 | Password string `mapstructure:"password,omitempty"` 52 | Database string `mapstructure:"database"` 53 | SSL SSLConfig `mapstructure:"ssl"` 54 | Options map[string]string `mapstructure:"options"` 55 | } 56 | 57 | type HealthCheckConfig struct { 58 | Delay int `mapstructure:"delay"` 59 | Query string `mapstructure:"query"` 60 | } 61 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package config 16 | 17 | import ( 18 | "github.com/spf13/viper" 19 | 20 | "github.com/crunchydata/crunchy-proxy/common" 21 | "github.com/crunchydata/crunchy-proxy/util/log" 22 | ) 23 | 24 | var c Config 25 | 26 | func init() { 27 | viper.SetConfigType("yaml") 28 | viper.SetConfigName("config") 29 | viper.AddConfigPath("/etc/crunchy-proxy") 30 | viper.AddConfigPath(".") 31 | } 32 | 33 | func GetConfig() Config { 34 | return c 35 | } 36 | 37 | func GetNodes() map[string]common.Node { 38 | return c.Nodes 39 | } 40 | 41 | func GetProxyConfig() ProxyConfig { 42 | return c.Server.Proxy 43 | } 44 | 45 | func GetAdminConfig() AdminConfig { 46 | return c.Server.Admin 47 | } 48 | 49 | func GetPoolCapacity() int { 50 | return c.Pool.Capacity 51 | } 52 | 53 | func GetCredentials() common.Credentials { 54 | return c.Credentials 55 | } 56 | 57 | func GetHealthCheckConfig() common.HealthCheckConfig { 58 | return c.HealthCheck 59 | } 60 | 61 | func Get(key string) interface{} { 62 | return viper.Get(key) 63 | } 64 | 65 | func GetBool(key string) bool { 66 | return viper.GetBool(key) 67 | } 68 | 69 | func GetInt(key string) int { 70 | return viper.GetInt(key) 71 | } 72 | 73 | func GetString(key string) string { 74 | return viper.GetString(key) 75 | } 76 | 77 | func GetStringMapString(key string) map[string]string { 78 | return viper.GetStringMapString(key) 79 | } 80 | 81 | func GetStringMap(key string) map[string]interface{} { 82 | return viper.GetStringMap(key) 83 | } 84 | 85 | func GetStringSlice(key string) []string { 86 | return viper.GetStringSlice(key) 87 | } 88 | 89 | func IsSet(key string) bool { 90 | return viper.IsSet(key) 91 | } 92 | 93 | func Set(key string, value interface{}) { 94 | viper.Set(key, value) 95 | } 96 | 97 | type ProxyConfig struct { 98 | HostPort string `mapstructure:"hostport"` 99 | } 100 | 101 | type AdminConfig struct { 102 | HostPort string `mapstructure:"hostport"` 103 | } 104 | 105 | type ServerConfig struct { 106 | Admin AdminConfig `mapstructure:"admin"` 107 | Proxy ProxyConfig `mapstructure:"proxy"` 108 | } 109 | 110 | type PoolConfig struct { 111 | Capacity int `mapstructure:"capacity"` 112 | } 113 | 114 | type Adapter struct { 115 | AdapterType string `mapstructure:"adaptertype"` 116 | Metadata map[string]interface{} `mapstructure:"metadata"` 117 | } 118 | 119 | type Config struct { 120 | //Nodes map[string]common.Node `mapstructure:"nodes"` 121 | Server ServerConfig `mapstructure:"server"` 122 | Pool PoolConfig `mapstructure:"pool"` 123 | Nodes map[string]common.Node `mapstructure:"nodes"` 124 | Credentials common.Credentials `mapstructure:"credentials"` 125 | HealthCheck common.HealthCheckConfig `mapstructure:"healthcheck"` 126 | } 127 | 128 | func SetConfigPath(path string) { 129 | viper.SetConfigFile(path) 130 | } 131 | 132 | func ReadConfig() { 133 | err := viper.ReadInConfig() 134 | log.Debugf("Using configuration file: %s", viper.ConfigFileUsed()) 135 | 136 | if err != nil { 137 | log.Fatal(err.Error()) 138 | } 139 | 140 | err = viper.Unmarshal(&c) 141 | 142 | if err != nil { 143 | log.Errorf("Error unmarshaling configuration file: %s", viper.ConfigFileUsed()) 144 | log.Fatalf(err.Error()) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /connect/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package connect 16 | 17 | import ( 18 | "bytes" 19 | "crypto/md5" 20 | "encoding/binary" 21 | "fmt" 22 | "io" 23 | "net" 24 | 25 | "github.com/crunchydata/crunchy-proxy/config" 26 | "github.com/crunchydata/crunchy-proxy/protocol" 27 | "github.com/crunchydata/crunchy-proxy/util/log" 28 | ) 29 | 30 | /* 31 | * Handle authentication requests that are sent by the backend to the client. 32 | * 33 | * connection - the connection to authenticate against. 34 | * message - the authentication message sent by the backend. 35 | */ 36 | func HandleAuthenticationRequest(connection net.Conn, message []byte) bool { 37 | var msgLength int32 38 | var authType int32 39 | 40 | // Read message length. 41 | reader := bytes.NewReader(message[1:5]) 42 | binary.Read(reader, binary.BigEndian, &msgLength) 43 | 44 | // Read authentication type. 45 | reader.Reset(message[5:9]) 46 | binary.Read(reader, binary.BigEndian, &authType) 47 | 48 | switch authType { 49 | case protocol.AuthenticationKerberosV5: 50 | log.Error("KerberosV5 authentication is not currently supported.") 51 | case protocol.AuthenticationClearText: 52 | log.Info("Authenticating with clear text password.") 53 | return handleAuthClearText(connection) 54 | case protocol.AuthenticationMD5: 55 | log.Info("Authenticating with MD5 password.") 56 | return handleAuthMD5(connection, message) 57 | case protocol.AuthenticationSCM: 58 | log.Error("SCM authentication is not currently supported.") 59 | case protocol.AuthenticationGSS: 60 | log.Error("GSS authentication is not currently supported.") 61 | case protocol.AuthenticationGSSContinue: 62 | log.Error("GSS authentication is not currently supported.") 63 | case protocol.AuthenticationSSPI: 64 | log.Error("SSPI authentication is not currently supported.") 65 | case protocol.AuthenticationOk: 66 | /* Covers the case where the authentication type is 'cert' or 'trust' */ 67 | return true 68 | default: 69 | log.Errorf("Unknown authentication method: %d", authType) 70 | } 71 | 72 | return false 73 | } 74 | 75 | func createMD5Password(username string, password string, salt string) string { 76 | // Concatenate the password and the username together. 77 | passwordString := fmt.Sprintf("%s%s", password, username) 78 | 79 | // Compute the MD5 sum of the password+username string. 80 | passwordString = fmt.Sprintf("%x", md5.Sum([]byte(passwordString))) 81 | 82 | // Compute the MD5 sum of the password hash and the salt 83 | passwordString = fmt.Sprintf("%s%s", passwordString, salt) 84 | return fmt.Sprintf("md5%x", md5.Sum([]byte(passwordString))) 85 | } 86 | 87 | func handleAuthMD5(connection net.Conn, message []byte) bool { 88 | // Get the authentication credentials. 89 | creds := config.GetCredentials() 90 | username := creds.Username 91 | password := creds.Password 92 | salt := string(message[9:13]) 93 | 94 | password = createMD5Password(username, password, salt) 95 | 96 | // Create the password message. 97 | passwordMessage := protocol.CreatePasswordMessage(password) 98 | 99 | // Send the password message to the backend. 100 | _, err := Send(connection, passwordMessage) 101 | 102 | // Check that write was successful. 103 | if err != nil { 104 | log.Error("Error sending password message to the backend.") 105 | log.Errorf("Error: %s", err.Error()) 106 | } 107 | 108 | // Read response from password message. 109 | message, _, err = Receive(connection) 110 | 111 | // Check that read was successful. 112 | if err != nil { 113 | log.Error("Error receiving authentication response from the backend.") 114 | log.Errorf("Error: %s", err.Error()) 115 | } 116 | 117 | return protocol.IsAuthenticationOk(message) 118 | } 119 | 120 | func handleAuthClearText(connection net.Conn) bool { 121 | password := config.GetString("credentials.password") 122 | passwordMessage := protocol.CreatePasswordMessage(password) 123 | 124 | _, err := connection.Write(passwordMessage) 125 | 126 | if err != nil { 127 | log.Error("Error sending clear text password message to the backend.") 128 | log.Errorf("Error: %s", err.Error()) 129 | } 130 | 131 | response := make([]byte, 4096) 132 | _, err = connection.Read(response) 133 | 134 | if err != nil { 135 | log.Error("Error receiving clear text authentication response.") 136 | log.Errorf("Error: %s", err.Error()) 137 | } 138 | 139 | return protocol.IsAuthenticationOk(response) 140 | } 141 | 142 | // AuthenticateClient - Establish and authenticate client connection to the backend. 143 | // 144 | // This function simply handles the passing of messages from the client to the 145 | // backend necessary for startup/authentication of a connection. All 146 | // communication is between the client and the master node. If the client 147 | // authenticates successfully with the master node, then 'true' is returned and 148 | // the authenticating connection is terminated. 149 | func AuthenticateClient(client net.Conn, message []byte, length int) (bool, error) { 150 | var err error 151 | 152 | nodes := config.GetNodes() 153 | 154 | node := nodes["master"] 155 | 156 | /* Establish a connection with the master node. */ 157 | log.Debug("client auth: connecting to 'master' node") 158 | master, err := Connect(node.HostPort) 159 | 160 | if err != nil { 161 | log.Error("An error occurred connecting to the master node") 162 | log.Errorf("Error %s", err.Error()) 163 | return false, err 164 | } 165 | 166 | defer master.Close() 167 | 168 | /* Relay the startup message to master node. */ 169 | log.Debug("client auth: relay startup message to 'master' node") 170 | _, err = master.Write(message[:length]) 171 | 172 | /* Receive startup response. */ 173 | log.Debug("client auth: receiving startup response from 'master' node") 174 | message, length, err = Receive(master) 175 | 176 | if err != nil { 177 | log.Error("An error occurred receiving startup response.") 178 | log.Errorf("Error %s", err.Error()) 179 | return false, err 180 | } 181 | 182 | /* 183 | * While the response for the master node is not an AuthenticationOK or 184 | * ErrorResponse keep relaying the mesages to/from the client/master. 185 | */ 186 | messageType := protocol.GetMessageType(message) 187 | 188 | for !protocol.IsAuthenticationOk(message) && 189 | (messageType != protocol.ErrorMessageType) { 190 | Send(client, message[:length]) 191 | message, length, err = Receive(client) 192 | 193 | /* 194 | * Must check that the client has not closed the connection. This in 195 | * particular is specific to 'psql' when it prompts for a password. 196 | * Apparently, when psql prompts the user for a password it closes the 197 | * original connection, and then creates a new one. Eventually the 198 | * following send/receives would timeout and no 'meaningful' messages 199 | * are relayed. This would ultimately cause an infinite loop. Thus it 200 | * is better to short circuit here if the client connection has been 201 | * closed. 202 | */ 203 | if (err != nil) && (err == io.EOF) { 204 | log.Info("The client closed the connection.") 205 | log.Debug("If the client is 'psql' and the authentication method " + 206 | "was 'password', then this behavior is expected.") 207 | return false, err 208 | } 209 | 210 | Send(master, message[:length]) 211 | 212 | message, length, err = Receive(master) 213 | 214 | messageType = protocol.GetMessageType(message) 215 | } 216 | 217 | /* 218 | * If the last response from the master node was AuthenticationOK, then 219 | * terminate the connection and return 'true' for a successful 220 | * authentication of the client. 221 | */ 222 | log.Debug("client auth: checking authentication repsonse") 223 | if protocol.IsAuthenticationOk(message) { 224 | termMsg := protocol.GetTerminateMessage() 225 | Send(master, termMsg) 226 | Send(client, message[:length]) 227 | return true, nil 228 | } 229 | 230 | if protocol.GetMessageType(message) == protocol.ErrorMessageType { 231 | err = protocol.ParseError(message) 232 | log.Error("Error occurred on client startup.") 233 | log.Errorf("Error: %s", err.Error()) 234 | } else { 235 | log.Error("Unknown error occurred on client startup.") 236 | } 237 | 238 | Send(client, message[:length]) 239 | 240 | return false, err 241 | } 242 | 243 | func ValidateClient(message []byte) bool { 244 | var clientUser string 245 | var clientDatabase string 246 | 247 | creds := config.GetCredentials() 248 | 249 | startup := protocol.NewMessageBuffer(message) 250 | 251 | startup.Seek(8) // Seek past the message length and protocol version. 252 | 253 | for { 254 | param, err := startup.ReadString() 255 | 256 | if err == io.EOF || param == "\x00" { 257 | break 258 | } 259 | 260 | switch param { 261 | case "user": 262 | clientUser, err = startup.ReadString() 263 | case "database": 264 | clientDatabase, err = startup.ReadString() 265 | } 266 | } 267 | 268 | return (clientUser == creds.Username && clientDatabase == creds.Database) 269 | } 270 | -------------------------------------------------------------------------------- /connect/connect.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package connect 16 | 17 | import ( 18 | "net" 19 | 20 | "github.com/crunchydata/crunchy-proxy/config" 21 | "github.com/crunchydata/crunchy-proxy/protocol" 22 | "github.com/crunchydata/crunchy-proxy/util/log" 23 | ) 24 | 25 | func Send(connection net.Conn, message []byte) (int, error) { 26 | return connection.Write(message) 27 | } 28 | 29 | func Receive(connection net.Conn) ([]byte, int, error) { 30 | buffer := make([]byte, 4096) 31 | length, err := connection.Read(buffer) 32 | return buffer, length, err 33 | } 34 | 35 | func Connect(host string) (net.Conn, error) { 36 | connection, err := net.Dial("tcp", host) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if config.GetBool("credentials.ssl.enable") { 43 | log.Info("SSL connections are enabled.") 44 | 45 | /* 46 | * First determine if SSL is allowed by the backend. To do this, send an 47 | * SSL request. The response from the backend will be a single byte 48 | * message. If the value is 'S', then SSL connections are allowed and an 49 | * upgrade to the connection should be attempted. If the value is 'N', 50 | * then the backend does not support SSL connections. 51 | */ 52 | 53 | /* Create the SSL request message. */ 54 | message := protocol.NewMessageBuffer([]byte{}) 55 | message.WriteInt32(8) 56 | message.WriteInt32(protocol.SSLRequestCode) 57 | 58 | /* Send the SSL request message. */ 59 | _, err := connection.Write(message.Bytes()) 60 | 61 | if err != nil { 62 | log.Error("Error sending SSL request to backend.") 63 | log.Errorf("Error: %s", err.Error()) 64 | return nil, err 65 | } 66 | 67 | /* Receive SSL response message. */ 68 | response := make([]byte, 4096) 69 | _, err = connection.Read(response) 70 | 71 | if err != nil { 72 | log.Error("Error receiving SSL response from backend.") 73 | log.Errorf("Error: %s", err.Error()) 74 | return nil, err 75 | } 76 | 77 | /* 78 | * If SSL is not allowed by the backend then close the connection and 79 | * throw an error. 80 | */ 81 | if len(response) > 0 && response[0] != 'S' { 82 | log.Error("The backend does not allow SSL connections.") 83 | connection.Close() 84 | } else { 85 | log.Debug("SSL connections are allowed by PostgreSQL.") 86 | log.Debug("Attempting to upgrade connection.") 87 | connection = UpgradeClientConnection(host, connection) 88 | log.Debug("Connection successfully upgraded.") 89 | } 90 | } 91 | 92 | return connection, nil 93 | } 94 | -------------------------------------------------------------------------------- /connect/ssl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package connect 17 | 18 | import ( 19 | "crypto/tls" 20 | "crypto/x509" 21 | "io/ioutil" 22 | "net" 23 | 24 | "github.com/crunchydata/crunchy-proxy/config" 25 | "github.com/crunchydata/crunchy-proxy/util/log" 26 | ) 27 | 28 | /* SSL constants. */ 29 | const ( 30 | SSL_REQUEST_CODE int32 = 80877103 31 | 32 | /* SSL Responses */ 33 | SSL_ALLOWED byte = 'S' 34 | SSL_NOT_ALLOWED byte = 'N' 35 | 36 | /* SSL Modes */ 37 | SSL_MODE_REQUIRE string = "require" 38 | SSL_MODE_VERIFY_CA string = "verify-ca" 39 | SSL_MODE_VERIFY_FULL string = "verify-full" 40 | SSL_MODE_DISABLE string = "disable" 41 | ) 42 | 43 | /* 44 | * 45 | */ 46 | func UpgradeServerConnection(client net.Conn) net.Conn { 47 | creds := config.GetCredentials() 48 | 49 | if creds.SSL.Enable { 50 | tlsConfig := tls.Config{} 51 | 52 | cert, _ := tls.LoadX509KeyPair( 53 | creds.SSL.SSLServerCert, 54 | creds.SSL.SSLServerKey) 55 | 56 | tlsConfig.Certificates = []tls.Certificate{cert} 57 | 58 | client = tls.Server(client, &tlsConfig) 59 | } 60 | 61 | return client 62 | } 63 | 64 | /* 65 | * 66 | */ 67 | func UpgradeClientConnection(hostPort string, connection net.Conn) net.Conn { 68 | verifyCA := false 69 | hostname, _, _ := net.SplitHostPort(hostPort) 70 | tlsConfig := tls.Config{} 71 | creds := config.GetCredentials() 72 | 73 | /* 74 | * Configure the connection based on the mode specificed in the proxy 75 | * configuration. Valid mode options are 'require', 'verify-ca', 76 | * 'verify-full' and 'disable'. Any other value will result in a fatal 77 | * error. 78 | */ 79 | switch creds.SSL.SSLMode { 80 | 81 | case SSL_MODE_REQUIRE: 82 | tlsConfig.InsecureSkipVerify = true 83 | 84 | /* 85 | * According to the documentation provided by 86 | * https://www.postgresql.org/docs/current/static/libpq-ssl.html, for 87 | * backwards compatibility with earlier version of PostgreSQL, if the 88 | * root CA file exists, then the behavior of 'sslmode=require' needs to 89 | * be the same as 'sslmode=verify-ca'. 90 | */ 91 | verifyCA = (creds.SSL.SSLRootCA != "") 92 | case SSL_MODE_VERIFY_CA: 93 | tlsConfig.InsecureSkipVerify = true 94 | verifyCA = true 95 | case SSL_MODE_VERIFY_FULL: 96 | tlsConfig.ServerName = hostname 97 | case SSL_MODE_DISABLE: 98 | return connection 99 | default: 100 | log.Fatalf("Unsupported sslmode %s\n", creds.SSL.SSLMode) 101 | } 102 | 103 | /* Add client SSL certificate and key. */ 104 | log.Debug("Loading SSL certificate and key") 105 | cert, _ := tls.LoadX509KeyPair(creds.SSL.SSLCert, creds.SSL.SSLKey) 106 | tlsConfig.Certificates = []tls.Certificate{cert} 107 | 108 | /* Add root CA certificate. */ 109 | log.Debug("Loading root CA.") 110 | tlsConfig.RootCAs = x509.NewCertPool() 111 | rootCA, _ := ioutil.ReadFile(creds.SSL.SSLRootCA) 112 | tlsConfig.RootCAs.AppendCertsFromPEM(rootCA) 113 | 114 | /* Upgrade the connection. */ 115 | log.Info("Upgrading to SSL connection.") 116 | client := tls.Client(connection, &tlsConfig) 117 | 118 | if verifyCA { 119 | log.Debug("Verify CA is enabled") 120 | err := verifyCertificateAuthority(client, &tlsConfig) 121 | if err != nil { 122 | log.Fatalf("Could not verify certificate authority: %s", err.Error()) 123 | } else { 124 | log.Info("Successfully verified CA") 125 | } 126 | } 127 | 128 | return client 129 | } 130 | 131 | /* 132 | * This function will perform a TLS handshake with the server and to verify the 133 | * certificates against the CA. 134 | * 135 | * client - the TLS client connection. 136 | * tlsConfig - the configuration associated with the connection. 137 | */ 138 | func verifyCertificateAuthority(client *tls.Conn, tlsConf *tls.Config) error { 139 | err := client.Handshake() 140 | 141 | if err != nil { 142 | return err 143 | } 144 | 145 | /* Get the peer certificates. */ 146 | certs := client.ConnectionState().PeerCertificates 147 | 148 | /* Setup the verification options. */ 149 | options := x509.VerifyOptions{ 150 | DNSName: client.ConnectionState().ServerName, 151 | Intermediates: x509.NewCertPool(), 152 | Roots: tlsConf.RootCAs, 153 | } 154 | 155 | for i, certificate := range certs { 156 | /* 157 | * The first certificate in the list is client certificate and not an 158 | * intermediate certificate. Therefore it should not be added. 159 | */ 160 | if i == 0 { 161 | continue 162 | } 163 | 164 | options.Intermediates.AddCert(certificate) 165 | } 166 | 167 | /* Verify the client certificate. 168 | * 169 | * The first certificate in the certificate to verify. 170 | */ 171 | _, err = certs[0].Verify(options) 172 | 173 | return err 174 | } 175 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Crunchy Proxy Documentation 2 | 3 | All project documentation is written in [asciidoc](http://asciidoc.org/). 4 | 5 | ## Requirements 6 | 7 | * [asciidoctor](http://asciidoctor.org/) 8 | * [asciidoctor-pdf](https://github.com/asciidoctor/asciidoctor-pdf) 9 | * [asciidoc-bootstrap-backend](https://github.com/llaville/asciidoc-bootstrap-backend) 10 | 11 | ## Building Documentation 12 | 13 | From the project root directory: 14 | 15 | ``` 16 | $> make docs 17 | ``` 18 | 19 | or from the 'docs' (this) directory: 20 | 21 | ``` 22 | $> ./build-docs.sh 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Build Users Guide 17 | asciidoctor-pdf ./crunchy-proxy-user-guide.asciidoc \ 18 | --out-file ./pdf/crunchy-proxy-user-guide.pdf 19 | 20 | # Build SSL Guide 21 | asciidoctor-pdf ./crunchy-proxy-ssl-guide.asciidoc \ 22 | --out-file ./pdf/crunchy-proxy-ssl-guide.pdf 23 | -------------------------------------------------------------------------------- /docs/crunchy-proxy-ssl-guide.asciidoc: -------------------------------------------------------------------------------- 1 | = SSL Guide - Crunchy Proxy 2 | Crunchy Data Solutions, Inc. 3 | v0.0.1, {docdate} 4 | :title-logo-image: image:crunchy_logo.png["CrunchyData Logo", align="center", scalewidth="80%"] 5 | 6 | == Overview 7 | 8 | Crunchy Proxy supports SSL connections as well as Certificate based 9 | authentication. 10 | 11 | The general concept is that since the proxy behaves as both a 'server' and a 12 | 'client', it must be configured both from an SSL perspective. Therefore, if 13 | this feature is enabled, then certificates for both scenarios must be available 14 | to the proxy. 15 | 16 | When enabled, all pool connections between the proxy and the backends will be a 17 | TLS connection. As well, the temporary connection used to authenticate a 18 | connecting client will also utilize a TLS connection to communicate with the 19 | backend performing the authentications. 20 | 21 | If this feature is enable, the proxy will return the appropriate message to the 22 | client that SSL connections are allowed. Therefore, it is up to the connecting 23 | client to choose whether or not it would like to proceed with making a 24 | connection or not. 25 | 26 | == SSL Certificates 27 | 28 | For testing purposes certificates have been provided in the projects 'certs' 29 | directory. As well, scripts have been provided to generate new certificates for 30 | both the server and client. These scripts will create a CA, Intermediate CA's 31 | and the required server and client certificates. The resulting server 32 | certificate is a wildcard certificate so that it can be used across the proxy, 33 | the master and the replicas. 34 | 35 | == Configuration 36 | 37 | The SSL configuration can be found within the 'credentials' section of the 38 | proxy configuration file. The following table enumerates and describes each 39 | configuration option. *Note*: '_enable_' and '_sslmode_' are required fields. 40 | 41 | [width="90%",cols="30,40,30",frame="topbot",options="header,footer"] 42 | |=== 43 | | Parameter 44 | | Type 45 | | Description 46 | 47 | | enable 48 | | boolean 49 | | Enables SSL connections through the proxy. Set true to allow, otherwise false 50 | *(required)*. 51 | 52 | | sslmode 53 | | string 54 | | The client SSL mode. Valid values are 'disable', 'require', 'verify-ca' and 55 | 'verify-full' *(required)*. 56 | 57 | | sslcert 58 | | string 59 | | The path to the client certificate. 60 | 61 | | sslkey 62 | | string 63 | | The path to the client key. 64 | 65 | | sslrootca 66 | | string 67 | | The path to the client root CA certificate. 68 | 69 | | sslservercert 70 | | string 71 | | The path to the server certificate. 72 | 73 | | sslserverkey 74 | | string 75 | | The path to the server key. 76 | 77 | | sslserverca 78 | | string 79 | | The path to the server root CA certificate. 80 | |=== 81 | 82 | *Example:* 83 | 84 | .... 85 | [...] 86 | credentials: 87 | username: postgres 88 | database": postgres 89 | ssl: 90 | enable: true 91 | sslmode: require 92 | sslcert: ./certs/client/client.crt 93 | sslkey: ./certs/client/client.key 94 | sslrootca: ./certs/client/ca.crt 95 | sslervercert: ./certs/server/server.crt 96 | sslserverkey: ./certs/server/server.key 97 | sslserverca: ./certs/server/ca.crt 98 | [...] 99 | .... 100 | 101 | == Example Usage 102 | 103 | *Connecting with _psql_:* 104 | 105 | .... 106 | $> psql "host=proxy.crunchy.lab port=5432 dbname=postgres user=postgres 107 | sslmode=verify-ca sslcert=./certs/client/client.crt 108 | sslkey=./certs/client/client.key sslrootcert=./certs/client/ca.crt" 109 | .... 110 | -------------------------------------------------------------------------------- /docs/crunchy_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrunchyData/crunchy-proxy/64e9426fd4ad77ec1652850d607a23a1201468a5/docs/crunchy_logo.png -------------------------------------------------------------------------------- /docs/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
Crunchy Data Solutions, Inc.
6 | -------------------------------------------------------------------------------- /docs/proxy-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrunchyData/crunchy-proxy/64e9426fd4ad77ec1652850d607a23a1201468a5/docs/proxy-diagram.png -------------------------------------------------------------------------------- /docs/proxy-golang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrunchyData/crunchy-proxy/64e9426fd4ad77ec1652850d607a23a1201468a5/docs/proxy-golang.png -------------------------------------------------------------------------------- /examples/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | proxy: 3 | hostport: 127.0.0.1:5432 4 | admin: 5 | hostport: 127.0.0.1:8000 6 | 7 | nodes: 8 | master: 9 | hostport: 192.168.56.100:5432 10 | role: master 11 | metadata: {} 12 | replica1: 13 | hostport: 192.168.56.101:5432 14 | role: replica 15 | metadata: {} 16 | 17 | credentials: 18 | username: postgres 19 | database: postgres 20 | password: password 21 | options: 22 | ssl: 23 | enable: false 24 | sslmode: disable 25 | 26 | pool: 27 | capacity: 2 28 | 29 | healthcheck: 30 | delay: 60 31 | query: select now(); 32 | -------------------------------------------------------------------------------- /examples/example-config-ssl.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | proxy: 3 | hostport: proxy.crunchy.lab:5432 4 | admin: 5 | hostport: localhost:10000 6 | nodes: 7 | master: 8 | hostport: master.crunchy.lab:5432 9 | role: master 10 | metadata: 11 | cpu: large 12 | disk: fast 13 | replica: 14 | role: replica 15 | hostport: replica.crunchy.lab:5432 16 | metadata: 17 | cpu: small 18 | disk: slow 19 | 20 | healthcheck: 21 | delay: 60 22 | query: select now(); 23 | 24 | pool: 25 | capacity: 2 26 | 27 | credentials: 28 | username: postgres 29 | database: postgres 30 | password: password 31 | options: 32 | application_name: crunchy-proxy 33 | ssl: 34 | enable: true 35 | sslmode: require 36 | sslcert: ./scripts/certs/client/client.crt 37 | sslkey: ./scripts/certs/client/client.key 38 | sslrootca: ./scripts/certs/client/ca.crt 39 | sslservercert: ./scripts/certs/server/server.crt 40 | sslserverkey: ./scripts/certs/server/server.key 41 | sslserverca: ./scripts/certs/server/ca.crt 42 | 43 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 42b00ced64c79f084618beca01d954a1686d103bdfef37a9de6630f955729019 2 | updated: 2017-06-23T14:28:50.748969094-04:00 3 | imports: 4 | - name: github.com/fsnotify/fsnotify 5 | version: 4da3e2cfbabc9f751898f250b49f2439785783a1 6 | - name: github.com/golang/protobuf 7 | version: 4f95b0d3eab845462f9b0ca5ad21dea8093be211 8 | subpackages: 9 | - proto 10 | - protoc-gen-go/descriptor 11 | - ptypes/any 12 | - name: github.com/hashicorp/hcl 13 | version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca 14 | subpackages: 15 | - hcl/ast 16 | - hcl/parser 17 | - hcl/scanner 18 | - hcl/strconv 19 | - hcl/token 20 | - json/parser 21 | - json/scanner 22 | - json/token 23 | - name: github.com/inconshreveable/mousetrap 24 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 25 | - name: github.com/lib/pq 26 | version: 8837942c3e09574accbc5f150e2c5e057189cace 27 | subpackages: 28 | - oid 29 | - name: github.com/magiconair/properties 30 | version: 51463bfca2576e06c62a8504b5c0f06d61312647 31 | - name: github.com/mitchellh/mapstructure 32 | version: d0303fe809921458f417bcf828397a65db30a7e4 33 | - name: github.com/pelletier/go-buffruneio 34 | version: c37440a7cf42ac63b919c752ca73a85067e05992 35 | - name: github.com/pelletier/go-toml 36 | version: fe7536c3dee2596cdd23ee9976a17c22bdaae286 37 | - name: github.com/pkg/errors 38 | version: c605e284fe17294bda444b34710735b29d1a9d90 39 | - name: github.com/Sirupsen/logrus 40 | version: 85b1699d505667d13f8ac4478c1debbf85d6c5de 41 | - name: github.com/spf13/afero 42 | version: 9be650865eab0c12963d8753212f4f9c66cdcf12 43 | subpackages: 44 | - mem 45 | - name: github.com/spf13/cast 46 | version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 47 | - name: github.com/spf13/cobra 48 | version: b4dbd37a01839e0653eec12aa4bbb2a2ce7b2a37 49 | - name: github.com/spf13/jwalterweatherman 50 | version: 0efa5202c04663c757d84f90f5219c1250baf94f 51 | - name: github.com/spf13/pflag 52 | version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 53 | - name: github.com/spf13/viper 54 | version: a1ecfa6a20bd4ef9e9caded262ee1b1b26847675 55 | - name: golang.org/x/net 56 | version: dfe83d419c9403b40b19d08cdba2afec27b002f7 57 | subpackages: 58 | - context 59 | - http2 60 | - http2/hpack 61 | - idna 62 | - internal/timeseries 63 | - lex/httplex 64 | - trace 65 | - name: golang.org/x/sys 66 | version: 0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79 67 | subpackages: 68 | - unix 69 | - name: golang.org/x/text 70 | version: 3491b61b9edc56653ad4333e605e2908e46a036b 71 | subpackages: 72 | - secure/bidirule 73 | - transform 74 | - unicode/bidi 75 | - unicode/norm 76 | - name: google.golang.org/genproto 77 | version: aa2eb687b4d3e17154372564ad8d6bf11c3cf21f 78 | subpackages: 79 | - googleapis/api/annotations 80 | - googleapis/rpc/status 81 | - name: google.golang.org/grpc 82 | version: d8960bd63c6743defa6d54926e5edfa8e4a28e43 83 | subpackages: 84 | - codes 85 | - credentials 86 | - grpclb/grpc_lb_v1 87 | - grpclog 88 | - internal 89 | - keepalive 90 | - metadata 91 | - naming 92 | - peer 93 | - stats 94 | - status 95 | - tap 96 | - transport 97 | - name: gopkg.in/yaml.v2 98 | version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 99 | testImports: [] 100 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/crunchydata/crunchy-proxy 2 | import: 3 | - package: github.com/golang/protobuf 4 | subpackages: 5 | - proto 6 | - package: golang.org/x/net 7 | subpackages: 8 | - context 9 | - package: google.golang.org/genproto 10 | subpackages: 11 | - googleapis/api/annotations 12 | - package: google.golang.org/grpc 13 | version: =1.4.0 14 | - package: github.com/spf13/cobra 15 | 16 | ignore: 17 | - scripts 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/crunchydata/crunchy-proxy/cli" 19 | ) 20 | 21 | func main() { 22 | cli.Main() 23 | } 24 | -------------------------------------------------------------------------------- /pool/pool.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package pool 16 | 17 | import ( 18 | "net" 19 | ) 20 | 21 | type Pool struct { 22 | connections chan net.Conn 23 | Name string 24 | Capacity int 25 | } 26 | 27 | func NewPool(name string, capacity int) *Pool { 28 | return &Pool{ 29 | connections: make(chan net.Conn, capacity), 30 | Name: name, 31 | Capacity: capacity, 32 | } 33 | } 34 | 35 | func (p *Pool) Add(connection net.Conn) { 36 | p.connections <- connection 37 | } 38 | 39 | func (p *Pool) Next() net.Conn { 40 | return <-p.connections 41 | } 42 | 43 | func (p *Pool) Return(connection net.Conn) { 44 | p.connections <- connection 45 | } 46 | 47 | func (p *Pool) Len() int { 48 | return len(p.connections) 49 | } 50 | -------------------------------------------------------------------------------- /protocol/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package protocol 16 | 17 | func CreatePasswordMessage(password string) []byte { 18 | message := NewMessageBuffer([]byte{}) 19 | 20 | /* Set the message type */ 21 | message.WriteByte(PasswordMessageType) 22 | 23 | /* Initialize the message length to zero. */ 24 | message.WriteInt32(0) 25 | 26 | /* Add the password to the message. */ 27 | message.WriteString(password) 28 | 29 | /* Update the message length */ 30 | message.ResetLength(PGMessageLengthOffset) 31 | 32 | return message.Bytes() 33 | } 34 | -------------------------------------------------------------------------------- /protocol/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package protocol 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | /* PG Error Severity Levels */ 22 | const ( 23 | ErrorSeverityFatal string = "FATAL" 24 | ErrorSeverityPanic string = "PANIC" 25 | ErrorSeverityWarning string = "WARNING" 26 | ErrorSeverityNotice string = "NOTICE" 27 | ErrorSeverityDebug string = "DEBUG" 28 | ErrorSeverityInfo string = "INFO" 29 | ErrorSeverityLog string = "LOG" 30 | ) 31 | 32 | /* PG Error Message Field Identifiers */ 33 | const ( 34 | ErrorFieldSeverity byte = 'S' 35 | ErrorFieldCode byte = 'C' 36 | ErrorFieldMessage byte = 'M' 37 | ErrorFieldMessageDetail byte = 'D' 38 | ErrorFieldMessageHint byte = 'H' 39 | ErrorFieldPosition byte = 'P' 40 | ErrorFieldInternalPosition byte = 'p' 41 | ErrorFieldInternalQuery byte = 'q' 42 | ErrorFieldWhere byte = 'W' 43 | ErrorFieldSchemaName byte = 's' 44 | ErrorFieldTableName byte = 't' 45 | ErrorFieldColumnName byte = 'c' 46 | ErrorFieldDataTypeName byte = 'd' 47 | ErrorFieldConstraintName byte = 'n' 48 | ErrorFieldFile byte = 'F' 49 | ErrorFieldLine byte = 'L' 50 | ErrorFieldRoutine byte = 'R' 51 | ) 52 | 53 | type Error struct { 54 | Severity string 55 | Code string 56 | Message string 57 | Detail string 58 | Hint string 59 | Position string 60 | InternalPosition string 61 | InternalQuery string 62 | Where string 63 | SchemaName string 64 | TableName string 65 | ColumnName string 66 | DataTypeName string 67 | Constraint string 68 | File string 69 | Line string 70 | Routine string 71 | } 72 | 73 | func (e *Error) Error() string { 74 | return fmt.Sprintf("pg: %s: %s", e.Severity, e.Message) 75 | } 76 | 77 | func (e *Error) GetMessage() []byte { 78 | msg := NewMessageBuffer([]byte{}) 79 | 80 | msg.WriteByte(ErrorMessageType) 81 | msg.WriteInt32(0) 82 | 83 | msg.WriteByte(ErrorFieldSeverity) 84 | msg.WriteString(e.Severity) 85 | 86 | msg.WriteByte(ErrorFieldCode) 87 | msg.WriteString(e.Code) 88 | 89 | msg.WriteByte(ErrorFieldMessage) 90 | msg.WriteString(e.Message) 91 | 92 | if e.Detail != "" { 93 | msg.WriteByte(ErrorFieldMessageDetail) 94 | msg.WriteString(e.Detail) 95 | } 96 | 97 | if e.Hint != "" { 98 | msg.WriteByte(ErrorFieldMessageHint) 99 | msg.WriteString(e.Hint) 100 | } 101 | 102 | msg.WriteByte(0x00) // null terminate the message 103 | 104 | msg.ResetLength(PGMessageLengthOffset) 105 | 106 | return msg.Bytes() 107 | } 108 | 109 | // ParseError parses a PG error message 110 | func ParseError(e []byte) *Error { 111 | msg := NewMessageBuffer(e) 112 | msg.Seek(5) 113 | err := new(Error) 114 | 115 | for field, _ := msg.ReadByte(); field != 0; field, _ = msg.ReadByte() { 116 | value, _ := msg.ReadString() 117 | switch field { 118 | case ErrorFieldSeverity: 119 | err.Severity = value 120 | case ErrorFieldCode: 121 | err.Code = value 122 | case ErrorFieldMessage: 123 | err.Message = value 124 | case ErrorFieldMessageDetail: 125 | err.Detail = value 126 | case ErrorFieldMessageHint: 127 | err.Hint = value 128 | case ErrorFieldPosition: 129 | err.Position = value 130 | case ErrorFieldInternalPosition: 131 | err.InternalPosition = value 132 | case ErrorFieldInternalQuery: 133 | err.InternalQuery = value 134 | case ErrorFieldWhere: 135 | err.Where = value 136 | case ErrorFieldSchemaName: 137 | err.SchemaName = value 138 | case ErrorFieldTableName: 139 | err.TableName = value 140 | case ErrorFieldColumnName: 141 | err.ColumnName = value 142 | case ErrorFieldDataTypeName: 143 | err.DataTypeName = value 144 | case ErrorFieldConstraintName: 145 | err.Constraint = value 146 | case ErrorFieldFile: 147 | err.File = value 148 | case ErrorFieldLine: 149 | err.Line = value 150 | case ErrorFieldRoutine: 151 | err.Routine = value 152 | } 153 | } 154 | return err 155 | } 156 | -------------------------------------------------------------------------------- /protocol/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package protocol 17 | 18 | import ( 19 | "bytes" 20 | "encoding/binary" 21 | "strings" 22 | ) 23 | 24 | /* PostgreSQL message length offset constants. */ 25 | const ( 26 | PGMessageLengthOffsetStartup int = 0 27 | PGMessageLengthOffset int = 1 28 | ) 29 | 30 | // MessageBuffer is a variable-sized byte buffer used to read and write 31 | // PostgreSQL Frontend and Backend messages. 32 | // 33 | // A separate instance of a MessageBuffer should be use for reading and writing. 34 | type MessageBuffer struct { 35 | buffer *bytes.Buffer 36 | } 37 | 38 | // NewMessageBuffer creates and intializes a new MessageBuffer using message as its 39 | // initial contents. 40 | func NewMessageBuffer(message []byte) *MessageBuffer { 41 | return &MessageBuffer{ 42 | buffer: bytes.NewBuffer(message), 43 | } 44 | } 45 | 46 | // ReadInt32 reads an int32 from the message buffer. 47 | // 48 | // This function will read the next 4 available bytes from the message buffer 49 | // and return them as an int32. If an error occurs then 0 and the error are 50 | // returned. 51 | func (message *MessageBuffer) ReadInt32() (int32, error) { 52 | value := make([]byte, 4) 53 | 54 | if _, err := message.buffer.Read(value); err != nil { 55 | return 0, err 56 | } 57 | 58 | return int32(binary.BigEndian.Uint32(value)), nil 59 | } 60 | 61 | // ReadInt16 reads an int16 from the message buffer. 62 | // 63 | // This function will read the next 2 available bytes from the message buffer 64 | // and return them as an int16. If an error occurs then 0 and the error are 65 | // returned. 66 | func (message *MessageBuffer) ReadInt16() (int16, error) { 67 | value := make([]byte, 2) 68 | 69 | if _, err := message.buffer.Read(value); err != nil { 70 | return 0, err 71 | } 72 | 73 | return int16(binary.BigEndian.Uint16(value)), nil 74 | } 75 | 76 | // ReadByte reads a byte from the message buffer. 77 | // 78 | // This function will read and return the next available byte from the message 79 | // buffer. 80 | func (message *MessageBuffer) ReadByte() (byte, error) { 81 | return message.buffer.ReadByte() 82 | } 83 | 84 | // ReadBytes reads a variable size byte array defined by count from the message 85 | // buffer. 86 | // 87 | // This function will read and return the number of bytes as specified by count. 88 | func (message *MessageBuffer) ReadBytes(count int) ([]byte, error) { 89 | value := make([]byte, count) 90 | 91 | if _, err := message.buffer.Read(value); err != nil { 92 | return nil, err 93 | } 94 | 95 | return value, nil 96 | } 97 | 98 | // ReadString reads a string from the message buffer. 99 | // 100 | // This function will read and return the next Null terminated string from the 101 | // message buffer. 102 | func (message *MessageBuffer) ReadString() (string, error) { 103 | str, err := message.buffer.ReadString(0x00) 104 | return strings.Trim(str, "\x00"), err 105 | } 106 | 107 | // WriteByte will write the specified byte to the message buffer. 108 | func (message *MessageBuffer) WriteByte(value byte) error { 109 | return message.buffer.WriteByte(value) 110 | } 111 | 112 | // WriteBytes writes a variable size byte array specified by 'value' to the 113 | // message buffer. 114 | // 115 | // This function will return the number of bytes written, if the buffer is not 116 | // large enough to hold the value then an error is returned. 117 | func (message *MessageBuffer) WriteBytes(value []byte) (int, error) { 118 | return message.buffer.Write(value) 119 | } 120 | 121 | // WriteInt16 will write a 2 byte int16 to the message buffer. 122 | func (message *MessageBuffer) WriteInt16(value int16) (int, error) { 123 | x := make([]byte, 2) 124 | binary.BigEndian.PutUint16(x, uint16(value)) 125 | return message.WriteBytes(x) 126 | } 127 | 128 | // WriteInt32 will write a 4 byte int32 to the message buffer. 129 | func (message *MessageBuffer) WriteInt32(value int32) (int, error) { 130 | x := make([]byte, 4) 131 | binary.BigEndian.PutUint32(x, uint32(value)) 132 | return message.WriteBytes(x) 133 | } 134 | 135 | // WriteString will write a NULL terminated string to the buffer. It is 136 | // assumed that the incoming string has *NOT* been NULL terminated. 137 | func (message *MessageBuffer) WriteString(value string) (int, error) { 138 | return message.buffer.WriteString((value + "\000")) 139 | } 140 | 141 | // ResetLength will reset the message length for the message. 142 | // 143 | // offset should be one of the PGMessageLengthOffset* constants. 144 | func (message *MessageBuffer) ResetLength(offset int) { 145 | /* Get the contents of the buffer. */ 146 | b := message.buffer.Bytes() 147 | 148 | /* Get the start of the message length bytes. */ 149 | s := b[offset:] 150 | 151 | /* Determine the new length and set it. */ 152 | binary.BigEndian.PutUint32(s, uint32(len(s))) 153 | } 154 | 155 | // Bytes gets the contents of the message buffer. This function is only 156 | // useful after 'Write' operations as the underlying implementation will return 157 | // the 'unread' portion of the buffer. 158 | func (message *MessageBuffer) Bytes() []byte { 159 | return message.buffer.Bytes() 160 | } 161 | 162 | // Reset resets the buffer to empty. 163 | func (message *MessageBuffer) Reset() { 164 | message.buffer.Reset() 165 | } 166 | 167 | // Seek moves the current position of the buffer. 168 | func (message *MessageBuffer) Seek(pos int) { 169 | message.buffer.Next(pos) 170 | } 171 | -------------------------------------------------------------------------------- /protocol/protocol.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package protocol 16 | 17 | import ( 18 | "bytes" 19 | "encoding/binary" 20 | ) 21 | 22 | /* PostgreSQL Protocol Version/Code constants */ 23 | const ( 24 | ProtocolVersion int32 = 196608 25 | SSLRequestCode int32 = 80877103 26 | 27 | /* SSL Responses */ 28 | SSLAllowed byte = 'S' 29 | SSLNotAllowed byte = 'N' 30 | ) 31 | 32 | /* PostgreSQL Message Type constants. */ 33 | const ( 34 | AuthenticationMessageType byte = 'R' 35 | ErrorMessageType byte = 'E' 36 | EmptyQueryMessageType byte = 'I' 37 | DescribeMessageType byte = 'D' 38 | RowDescriptionMessageType byte = 'T' 39 | DataRowMessageType byte = 'D' 40 | QueryMessageType byte = 'Q' 41 | CommandCompleteMessageType byte = 'C' 42 | TerminateMessageType byte = 'X' 43 | NoticeMessageType byte = 'N' 44 | PasswordMessageType byte = 'p' 45 | ReadyForQueryMessageType byte = 'Z' 46 | ) 47 | 48 | /* PostgreSQL Authentication Method constants. */ 49 | const ( 50 | AuthenticationOk int32 = 0 51 | AuthenticationKerberosV5 int32 = 2 52 | AuthenticationClearText int32 = 3 53 | AuthenticationMD5 int32 = 5 54 | AuthenticationSCM int32 = 6 55 | AuthenticationGSS int32 = 7 56 | AuthenticationGSSContinue int32 = 8 57 | AuthenticationSSPI int32 = 9 58 | ) 59 | 60 | func GetVersion(message []byte) int32 { 61 | var code int32 62 | 63 | reader := bytes.NewReader(message[4:8]) 64 | binary.Read(reader, binary.BigEndian, &code) 65 | 66 | return code 67 | } 68 | 69 | /* 70 | * Get the message type the provided message. 71 | * 72 | * message - the message 73 | */ 74 | func GetMessageType(message []byte) byte { 75 | return message[0] 76 | } 77 | 78 | /* 79 | * Get the message length of the provided message. 80 | * 81 | * message - the message 82 | */ 83 | func GetMessageLength(message []byte) int32 { 84 | var messageLength int32 85 | 86 | reader := bytes.NewReader(message[1:5]) 87 | binary.Read(reader, binary.BigEndian, &messageLength) 88 | 89 | return messageLength 90 | } 91 | 92 | /* IsAuthenticationOk 93 | * 94 | * Check an Authentication Message to determine if it is an AuthenticationOK 95 | * message. 96 | */ 97 | func IsAuthenticationOk(message []byte) bool { 98 | /* 99 | * If the message type is not an Authentication message, then short circuit 100 | * and return false. 101 | */ 102 | if GetMessageType(message) != AuthenticationMessageType { 103 | return false 104 | } 105 | 106 | var messageValue int32 107 | 108 | // Get the message length. 109 | messageLength := GetMessageLength(message) 110 | 111 | // Get the message value. 112 | reader := bytes.NewReader(message[5:9]) 113 | binary.Read(reader, binary.BigEndian, &messageValue) 114 | 115 | return (messageLength == 8 && messageValue == AuthenticationOk) 116 | } 117 | 118 | func GetTerminateMessage() []byte { 119 | var buffer []byte 120 | buffer = append(buffer, 'X') 121 | 122 | //make msg len 1 for now 123 | x := make([]byte, 4) 124 | binary.BigEndian.PutUint32(x, uint32(4)) 125 | buffer = append(buffer, x...) 126 | return buffer 127 | } 128 | -------------------------------------------------------------------------------- /protocol/startup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package protocol 16 | 17 | // CreateStartupMessage creates a PG startup message. This message is used to 18 | // startup all connections with a PG backend. 19 | func CreateStartupMessage(username string, database string, options map[string]string) []byte { 20 | message := NewMessageBuffer([]byte{}) 21 | 22 | /* Temporarily set the message length to 0. */ 23 | message.WriteInt32(0) 24 | 25 | /* Set the protocol version. */ 26 | message.WriteInt32(ProtocolVersion) 27 | 28 | /* 29 | * The protocol version number is followed by one or more pairs of 30 | * parameter name and value strings. A zero byte is required as a 31 | * terminator after the last name/value pair. Parameters can appear in any 32 | * order. 'user' is required, others are optional. 33 | */ 34 | 35 | /* Set the 'user' parameter. This is the only *required* parameter. */ 36 | message.WriteString("user") 37 | message.WriteString(username) 38 | 39 | /* 40 | * Set the 'database' parameter. If no database name has been specified, 41 | * then the default value is the user's name. 42 | */ 43 | message.WriteString("database") 44 | message.WriteString(database) 45 | 46 | /* Set the remaining options as specified. */ 47 | for option, value := range options { 48 | message.WriteString(option) 49 | message.WriteString(value) 50 | } 51 | 52 | /* The message should end with a NULL byte. */ 53 | message.WriteByte(0x00) 54 | 55 | /* update the msg len */ 56 | message.ResetLength(PGMessageLengthOffsetStartup) 57 | 58 | return message.Bytes() 59 | } 60 | -------------------------------------------------------------------------------- /proxy/annotation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package proxy 16 | 17 | const ( 18 | ReadAnnotation AnnotationType = iota 19 | StartAnnotation 20 | EndAnnotation 21 | ) 22 | 23 | const ( 24 | readAnnotationString string = "read" 25 | startAnnotationString string = "start" 26 | endAnnotationString string = "end" 27 | unknownAnnotationString string = "" 28 | ) 29 | 30 | const ( 31 | AnnotationStartToken = "/*" 32 | AnnotationEndToken = "*/" 33 | ) 34 | 35 | type AnnotationType int 36 | 37 | func (a AnnotationType) String() string { 38 | switch a { 39 | case ReadAnnotation: 40 | return readAnnotationString 41 | case StartAnnotation: 42 | return startAnnotationString 43 | case EndAnnotation: 44 | return endAnnotationString 45 | } 46 | 47 | return unknownAnnotationString 48 | } 49 | -------------------------------------------------------------------------------- /proxy/parse.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package proxy 17 | 18 | import ( 19 | "strings" 20 | 21 | "github.com/crunchydata/crunchy-proxy/protocol" 22 | ) 23 | 24 | // GetAnnotations the annotation approach 25 | // assume a write if there is no comment in the SQL 26 | // or if there are no keywords in the comment 27 | // return (write, start, finish) booleans 28 | func getAnnotations(m []byte) map[AnnotationType]bool { 29 | message := protocol.NewMessageBuffer(m) 30 | annotations := make(map[AnnotationType]bool, 0) 31 | 32 | /* Get the query string */ 33 | message.ReadByte() // read past the message type 34 | message.ReadInt32() // read past the message length 35 | query, _ := message.ReadString() 36 | 37 | /* Find the start and end position of the annotations. */ 38 | startPos := strings.Index(query, AnnotationStartToken) 39 | endPos := strings.Index(query, AnnotationEndToken) 40 | 41 | /* 42 | * If the start or end positions are less than zero then that means that 43 | * an annotation was not found. 44 | */ 45 | if startPos < 0 || endPos < 0 { 46 | return annotations 47 | } 48 | 49 | /* Deterimine which annotations were specified as part of the query */ 50 | keywords := strings.Split(query[startPos+2:endPos], ",") 51 | 52 | for i := 0; i < len(keywords); i++ { 53 | switch strings.TrimSpace(keywords[i]) { 54 | case readAnnotationString: 55 | annotations[ReadAnnotation] = true 56 | case startAnnotationString: 57 | annotations[StartAnnotation] = true 58 | case endAnnotationString: 59 | annotations[EndAnnotation] = true 60 | } 61 | } 62 | 63 | return annotations 64 | } 65 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package proxy 16 | 17 | import ( 18 | "io" 19 | "net" 20 | "sync" 21 | 22 | "github.com/crunchydata/crunchy-proxy/common" 23 | "github.com/crunchydata/crunchy-proxy/config" 24 | "github.com/crunchydata/crunchy-proxy/connect" 25 | "github.com/crunchydata/crunchy-proxy/pool" 26 | "github.com/crunchydata/crunchy-proxy/protocol" 27 | "github.com/crunchydata/crunchy-proxy/util/log" 28 | ) 29 | 30 | type Proxy struct { 31 | writePools chan *pool.Pool 32 | readPools chan *pool.Pool 33 | master common.Node 34 | clients []net.Conn 35 | Stats map[string]int32 36 | lock *sync.Mutex 37 | } 38 | 39 | func NewProxy() *Proxy { 40 | p := &Proxy{ 41 | Stats: make(map[string]int32), 42 | lock: &sync.Mutex{}, 43 | } 44 | 45 | p.setupPools() 46 | 47 | return p 48 | } 49 | 50 | func (p *Proxy) setupPools() { 51 | nodes := config.GetNodes() 52 | capacity := config.GetPoolCapacity() 53 | 54 | /* Initialize pool structures */ 55 | numNodes := len(nodes) 56 | p.writePools = make(chan *pool.Pool, numNodes) 57 | p.readPools = make(chan *pool.Pool, numNodes) 58 | 59 | for name, node := range nodes { 60 | /* Create Pool for Node */ 61 | newPool := pool.NewPool(name, capacity) 62 | 63 | if node.Role == common.NODE_ROLE_MASTER { 64 | p.writePools <- newPool 65 | } else { 66 | p.readPools <- newPool 67 | } 68 | 69 | /* Create connections and add to pool. */ 70 | for i := 0; i < capacity; i++ { 71 | /* Connect and authenticate */ 72 | log.Infof("Connecting to node '%s' at %s...", name, node.HostPort) 73 | connection, err := connect.Connect(node.HostPort) 74 | 75 | username := config.GetString("credentials.username") 76 | database := config.GetString("credentials.database") 77 | options := config.GetStringMapString("credentials.options") 78 | 79 | startupMessage := protocol.CreateStartupMessage(username, database, options) 80 | 81 | connection.Write(startupMessage) 82 | 83 | response := make([]byte, 4096) 84 | connection.Read(response) 85 | 86 | authenticated := connect.HandleAuthenticationRequest(connection, response) 87 | 88 | if !authenticated { 89 | log.Error("Authentication failed") 90 | } 91 | 92 | if err != nil { 93 | log.Errorf("Error establishing connection to node '%s'", name) 94 | log.Errorf("Error: %s", err.Error()) 95 | } else { 96 | log.Infof("Successfully connected to '%s' at '%s'", name, node.HostPort) 97 | newPool.Add(connection) 98 | } 99 | } 100 | } 101 | } 102 | 103 | // Get the next pool. If read is set to true, then a 'read-only' pool will be 104 | // returned. Otherwise, a 'read-write' pool will be returned. 105 | func (p *Proxy) getPool(read bool) *pool.Pool { 106 | if read { 107 | return <-p.readPools 108 | } 109 | return <-p.writePools 110 | } 111 | 112 | // Return the pool. If read is 'true' then, the pool will be returned to the 113 | // 'read-only' collection of pools. Otherwise, it will be returned to the 114 | // 'read-write' collection of pools. 115 | func (p *Proxy) returnPool(pl *pool.Pool, read bool) { 116 | if read { 117 | p.readPools <- pl 118 | } else { 119 | p.writePools <- pl 120 | } 121 | } 122 | 123 | // HandleConnection handle an incoming connection to the proxy 124 | func (p *Proxy) HandleConnection(client net.Conn) { 125 | /* Get the client startup message. */ 126 | message, length, err := connect.Receive(client) 127 | 128 | if err != nil { 129 | log.Error("Error receiving startup message from client.") 130 | log.Errorf("Error: %s", err.Error()) 131 | } 132 | 133 | /* Get the protocol from the startup message.*/ 134 | version := protocol.GetVersion(message) 135 | 136 | /* Handle the case where the startup message was an SSL request. */ 137 | if version == protocol.SSLRequestCode { 138 | sslResponse := protocol.NewMessageBuffer([]byte{}) 139 | 140 | /* Determine which SSL response to send to client. */ 141 | creds := config.GetCredentials() 142 | if creds.SSL.Enable { 143 | sslResponse.WriteByte(protocol.SSLAllowed) 144 | } else { 145 | sslResponse.WriteByte(protocol.SSLNotAllowed) 146 | } 147 | 148 | /* 149 | * Send the SSL response back to the client and wait for it to send the 150 | * regular startup packet. 151 | */ 152 | connect.Send(client, sslResponse.Bytes()) 153 | 154 | /* Upgrade the client connection if required. */ 155 | client = connect.UpgradeServerConnection(client) 156 | 157 | /* 158 | * Re-read the startup message from the client. It is possible that the 159 | * client might not like the response given and as a result it might 160 | * close the connection. This is not an 'error' condition as this is an 161 | * expected behavior from a client. 162 | */ 163 | if message, length, err = connect.Receive(client); err == io.EOF { 164 | log.Info("The client closed the connection.") 165 | return 166 | } 167 | } 168 | 169 | /* 170 | * Validate that the client username and database are the same as that 171 | * which is configured for the proxy connections. 172 | * 173 | * If the the client cannot be validated then send an appropriate PG error 174 | * message back to the client. 175 | */ 176 | if !connect.ValidateClient(message) { 177 | pgError := protocol.Error{ 178 | Severity: protocol.ErrorSeverityFatal, 179 | Code: protocol.ErrorCodeInvalidAuthorizationSpecification, 180 | Message: "could not validate user/database", 181 | } 182 | 183 | connect.Send(client, pgError.GetMessage()) 184 | log.Error("Could not validate client") 185 | return 186 | } 187 | 188 | /* Authenticate the client against the appropriate backend. */ 189 | log.Infof("Client: %s - authenticating", client.RemoteAddr()) 190 | authenticated, err := connect.AuthenticateClient(client, message, length) 191 | 192 | /* If the client could not authenticate then go no further. */ 193 | if err == io.EOF { 194 | return 195 | } else if !authenticated { 196 | log.Errorf("Client: %s - authentication failed", client.RemoteAddr()) 197 | log.Errorf("Error: %s", err.Error()) 198 | return 199 | } else { 200 | log.Debugf("Client: %s - authentication successful", client.RemoteAddr()) 201 | } 202 | 203 | /* Process the client messages for the life of the connection. */ 204 | var statementBlock bool 205 | var cp *pool.Pool // The connection pool in use 206 | var backend net.Conn // The backend connection in use 207 | var read bool 208 | var end bool 209 | var nodeName string 210 | 211 | for { 212 | var done bool // for message processing loop. 213 | 214 | message, length, err = connect.Receive(client) 215 | 216 | if err != nil { 217 | switch err { 218 | case io.EOF: 219 | log.Infof("Client: %s - closed the connection", client.RemoteAddr()) 220 | default: 221 | log.Errorf("Error reading from client connection %s", client.RemoteAddr()) 222 | log.Errorf("Error: %s", err.Error()) 223 | } 224 | break 225 | } 226 | 227 | messageType := protocol.GetMessageType(message) 228 | 229 | /* 230 | * If the message is a simple query, then it can have read/write 231 | * annotations attached to it. Therefore, we need to process it and 232 | * determine which backend we need to send it to. 233 | */ 234 | if messageType == protocol.TerminateMessageType { 235 | log.Infof("Client: %s - disconnected", client.RemoteAddr()) 236 | return 237 | } else if messageType == protocol.QueryMessageType { 238 | annotations := getAnnotations(message) 239 | 240 | if annotations[StartAnnotation] { 241 | statementBlock = true 242 | } else if annotations[EndAnnotation] { 243 | end = true 244 | statementBlock = false 245 | } 246 | 247 | read = annotations[ReadAnnotation] 248 | 249 | /* 250 | * If not in a statement block or if the pool or backend are not already 251 | * set, then fetch a new backend to receive the message. 252 | */ 253 | if !statementBlock && !end || cp == nil || backend == nil { 254 | cp = p.getPool(read) 255 | backend = cp.Next() 256 | nodeName = cp.Name 257 | p.returnPool(cp, read) 258 | } 259 | 260 | /* Update the query count for the node being used. */ 261 | p.lock.Lock() 262 | p.Stats[nodeName] += 1 263 | p.lock.Unlock() 264 | 265 | /* Relay message to client and backend */ 266 | if _, err = connect.Send(backend, message[:length]); err != nil { 267 | log.Debugf("Error sending message to backend %s", backend.RemoteAddr()) 268 | log.Debugf("Error: %s", err.Error()) 269 | } 270 | 271 | /* 272 | * Continue to read from the backend until a 'ReadyForQuery' message is 273 | * is found. 274 | */ 275 | for !done { 276 | if message, length, err = connect.Receive(backend); err != nil { 277 | log.Debugf("Error receiving response from backend %s", backend.RemoteAddr()) 278 | log.Debugf("Error: %s", err.Error()) 279 | done = true 280 | } 281 | 282 | messageType := protocol.GetMessageType(message[:length]) 283 | 284 | /* 285 | * Examine all of the messages in the buffer and determine if any of 286 | * them are a ReadyForQuery message. 287 | */ 288 | for start := 0; start < length; { 289 | messageType = protocol.GetMessageType(message[start:]) 290 | messageLength := protocol.GetMessageLength(message[start:]) 291 | 292 | /* 293 | * Calculate the next start position, add '1' to the message 294 | * length to account for the message type. 295 | */ 296 | start = (start + int(messageLength) + 1) 297 | } 298 | 299 | if _, err = connect.Send(client, message[:length]); err != nil { 300 | log.Debugf("Error sending response to client %s", client.RemoteAddr()) 301 | log.Debugf("Error: %s", err.Error()) 302 | done = true 303 | } 304 | 305 | done = (messageType == protocol.ReadyForQueryMessageType) 306 | } 307 | 308 | /* 309 | * If at the end of a statement block or not part of statment block, 310 | * then return the connection to the pool. 311 | */ 312 | if !statementBlock { 313 | /* 314 | * Toggle 'end' such that a new connection will be fetched on the 315 | * next query. 316 | */ 317 | if end { 318 | end = false 319 | } 320 | 321 | /* Return the backend to the pool it belongs to. */ 322 | cp.Return(backend) 323 | } 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .docker 2 | crunchyproxy 3 | -------------------------------------------------------------------------------- /scripts/certs/README: -------------------------------------------------------------------------------- 1 | # certs 2 | 3 | This directory contains scripts to create example SSL certificates for both 4 | server and client applications. 5 | 6 | * generate-ssl.sh 7 | 8 | * generate-client-cert.sh 9 | 10 | This directory is referenced by the provisioning scripts for testing purposes. 11 | If anything is updated here, then those scripts will need to be updated as well. 12 | 13 | ## server 14 | 15 | This directory contains example SSL server certificates. 16 | 17 | ## client 18 | 19 | This directory contains example SSL client certificates. 20 | -------------------------------------------------------------------------------- /scripts/certs/client/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFojCCA4qgAwIBAgIJAKNEPHLCT5cuMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 4 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 5 | MDIwOTAwNTIxMloXDTIyMDIwODAwNTIxMlowYTELMAkGA1UEBhMCVVMxCzAJBgNV 6 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 7 | YSBTb2x1dGlvbnMxEDAOBgNVBAMMB3Jvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUA 8 | A4ICDwAwggIKAoICAQDwEPViaE5R1tJWFccqL8R0lRgqI0E0dkOxpDahsmRowVGc 9 | k3oiOwN0AbxMa9Sb2kDsITv5IH4AG0hkt7UsEVsLiaJ1wLJTYzaAbHfzMTneYrpp 10 | aABTZaAhSdHdQ0uSwq2rKo/oyrrHcy7G+6wL+bxlv/EUkqrPH8gBaps/ZFTpD24h 11 | jANbKj3ZdUZqpqVh0TKiV6vrtHn/yRvldcYiuvonrBFXJMgFBRWK/MVmeQ5kn/Dj 12 | GVIjhQtD9AI9oyf7uAwm6s6NCJR6RiVRzkqw6UWM2+La/JUfZcp1oNjTolnwRtv1 13 | KyM77Fo/5TZYWiPFuYWtbBH7sPZfZUptka1/PPn8FZPfaL3mFNYWNj9/T1HCzXGp 14 | ICeI4fk+y9EanLcO54LcQEf5K9IVxQfBfA5D/ED+KVoJ82Q6LRJQcr2ZgP5HBcXn 15 | 76IXetboveXSKxNzNU8hMu+mCkm6RmMJ7ycaBoT2UOT5S++g+9zQzSmqkcWpky+D 16 | rZsBgOeNLVACjBwxBgL2E5WpuMyODrV32kPmVvhO/tHFmp07LvY4e0RLfxiexaTI 17 | nQbsBSqIusDScu0n2qaHoMN5nU+g/v+48uuF+l90BuxkOMn09k3K5glOsCQaBGEW 18 | bxhGhzdZZqjOwhbO2y5wFdondh/vMqtB+FY4gH9SZ5x856efBnRMYVou8GCZbQID 19 | AQABo10wWzAdBgNVHQ4EFgQUs70urBMb65yVT5/q5N9OQ4kse58wHwYDVR0jBBgw 20 | FoAUs70urBMb65yVT5/q5N9OQ4kse58wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMC 21 | AQYwDQYJKoZIhvcNAQELBQADggIBAAyHd5Mh3Pzyzy48TKJvYcna6HALwJV7CqhK 22 | g4Ff64qqquBhyjKIgC7WgdJw8hveApTP6g3uT5u0SC4BY/h+q0TacwFGZ6IMggHU 23 | TS8iBkBuagFXTtd0LFiigBvJVBv22IcY2tz/pPoS9eG2mMv/uaNLXx0Uor5DTvI1 24 | AbytUgQg0B/6ElDEZCG1Rvhs/ZOhx52extinw2X5HvVqPxMuwAUpo6Wp4vNn062l 25 | us8DVd4gW8t8RNZN5ktJ5XM4XZ2smG/wPDigpQ8OQtGKTZznrmv1+6O29F3cWXHF 26 | 4G3V7s2pi94ESW4VR8tzgH752+F2jBxp62H7foVacVLv3uADgqKP1YBGEuPJaaTG 27 | HsENxMxVKNJLssEV/+FOsr3me67qMyeWtI1bWHnyC6VvXAHMZrvTElVwqa2P9qEF 28 | II7UebrGSZUJZx+3WeoUC70a2B3N9bhFVaBbsMBqyg9K0Opj7BLXAL/GyFqj1Azj 29 | 7Ei0RydLw0PzreHBBkh/9Yq1Zc14nKlH/7STtRipjRoaHTrM/MVUd8yAHOSpPmUY 30 | KTJwWON+k2ZceUyApoN2ri5WaoacJjG/ct5tDIe3bfrK/wgArGENBSgWf07t+rMy 31 | 4K04KAwgxBoCoefeuQrvE8p8PUVneqNfUvV2DhdA0AD1lLUOV1OwfyRF9R52Zqof 32 | j14A3IDt 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /scripts/certs/client/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFxjCCA66gAwIBAgIJAKn4+mMQnVO/MA0GCSqGSIb3DQEBCwUAMGYxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 4 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRUwEwYDVQQDDAxjbGllbnQtaW0tY2Ew 5 | HhcNMTcwMjA5MDA1MjE0WhcNMjIwMjA4MDA1MjE0WjBiMQswCQYDVQQGEwJVUzEL 6 | MAkGA1UECAwCVkExEjAQBgNVBAcMCUFybGluZ3RvbjEfMB0GA1UECgwWQ3J1bmNo 7 | eSBEYXRhIFNvbHV0aW9uczERMA8GA1UEAwwIcG9zdGdyZXMwggIiMA0GCSqGSIb3 8 | DQEBAQUAA4ICDwAwggIKAoICAQCiel/C4qikhR+lcuPuS3jfnUyenGU2S2v8slDv 9 | w+hUbY/MD6dcH2cnoj+OcSzdCvf10LhNSNrlr31TiEOAjGPBliRW3kQK1it0qAZl 10 | zTLYEBVeVQfuAg3qHgOOAqx1ujemZSAZr7O3h4DvJ9wO8JkfKcqPKi8c+h2t6QCg 11 | fdKqcDN6/1dxLyiTc9ruGkcMDChca+0E5XR/EbpaMFpqKzo9fmMMNNkFX4w9wxB/ 12 | BdVoRkdcUxpUPDa12rmorHYg7ayCUmTK7WV4ewco4H1WICsJNT9OxaQCmSoTt11H 13 | HQ/lS1BAA74XP3AoK9r2CDl+CXX4BR7vK46rq+g8nPO3UdVgIQBOyFAso0DOzMQQ 14 | Opomd8BxnaRE9cVlqIx0PaFpZzpT2U2NUYhYvw0OUKim/qqK5yfJIzgIQe/+yOsR 15 | VryWrKImOB7StHMZt5gl9FNe7FEp/FJSw7YkTBtc+M3vK7sUluS5sL4poLCb8b1B 16 | eEPFJmk3NiYpVacvoT9PKxamtl6CVR9GlrAkj8AlA0fbwLN3sSBRsfLLIFOeSI5R 17 | EaEOqw3FiUMDbYCnBnlxef42o0j4tSshay3JJcqCWMPVtxzfwOQZ3W5L8r6PRvSW 18 | OdpjuazFESqnJpYLlrHNgXaTxSa+fUc+VJuz3Yg33bpftRcNW8OfYnFecDDU5+bj 19 | F2gu4wIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM 20 | IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUL50wpBAqUqlXtsX/ogyp 21 | 3W5iFNIwHwYDVR0jBBgwFoAUeo6xLu00z5mZul5XoXI+ZKWY21IwDQYJKoZIhvcN 22 | AQELBQADggIBAGvnc4PTdobQTfjE8zy4srMVcm7ArU+ZUeM0Ar8PgNcEmwPLefRU 23 | Py/6Rg53YZCWb3WPBGVaGUPFsvFBWCAkna9yEM2a/pZ5pO2MnTd/0PNMv5i6xVWE 24 | kd1qOtXQIPhF3J5oadQ115VtEIactaEZTFFYloN8uR651lebMcKmXWKJnHRfEMH5 25 | t1tOGyaY9d4nzDSOgvoTu0epVux/2v4KgSP5rSdPv2sQ1J5aOfaw7rJHN5pDNaWR 26 | wKPwxtKAL51ttyU9rwWtsQUPrg5WLHLpdAhbZggComIHf93LWoERdRuqiZNLY+06 27 | YIjUJUZ+49VJSUVxy9XRNeLfAYXQJUTJNkxRo1MuyM3fYYjAB+mHFfNgNl04gMES 28 | gFdEARoK9ySY/fbgtESxxHfJTXRG9mma6uKwimClRwGpJ+b3rScvrnE9LKnCT4fL 29 | lk2gDeAoKM8o9zhIsMwfBSg6J1+SmHuW+LNOAT0VjONbsNNbShZBFMCJnymvvbuS 30 | IqqS0veXBvwwsN+Bg00+mlltGyU/QECJhda2Aj1CGyiFuCpMOUHaxkkdRHdAeaS7 31 | Zs1zZ2Wh56nQWr9gwk8KsK8/tpnCsJgNaHK+H4W7Kjarg7tlFIa4+Gq1CUOCmDr5 32 | 1hu7fT78ywq1VrRLRLi9RX9KxP70SZrBbDInSGWLY7QtsiHIc+VIHILC 33 | -----END CERTIFICATE----- 34 | -----BEGIN CERTIFICATE----- 35 | MIIFpzCCA4+gAwIBAgIJAPfdFYztRKePMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 36 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 37 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 38 | MDIwOTAwNTIxM1oXDTIyMDIwODAwNTIxM1owZjELMAkGA1UEBhMCVVMxCzAJBgNV 39 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 40 | YSBTb2x1dGlvbnMxFTATBgNVBAMMDGNsaWVudC1pbS1jYTCCAiIwDQYJKoZIhvcN 41 | AQEBBQADggIPADCCAgoCggIBALbUGF6ncbty5WkJNleJJqQNd3jNKsjh6ZmdEIjd 42 | 9jb9bJctnb7egqVQRbpvkzdmhaPCXoAzHHDF3lmobVmobu/NOE7sWVP89NuXdTJa 43 | 8D7d1kjcRar7vaqjRCy4iKJi1fawwp9yTPgaQ7Q+NlwoeX0iZOrTAN3yJlqhzIle 44 | 4ANOc19BwZVLGZCKZMcRw/hZUINSHo7AZvzfY/JDL8+piaK1YtRy1SBXD8CvJ+zc 45 | ecvCC9rzS/NnU3YLcA4tLbtNF7EP899OqkZ/yTjrvkTZs7B2vHoe+R8nXPOGAXF9 46 | mZzP13yvfSs79xf5WmpvO/bfJ+Q8xcRnzsJ2Ed1ocJagut2kLODn6gqvCqq46DHq 47 | QqdBlHobIoqvbMbWeXJgTPp4Q85LB/nCmHoIig6HQV5HwER2qqPUzcb4KU6cbaXS 48 | yo0FiqiwNKhAkPWZ6XJkSQOMezheuKGd+4wvzIpwinyN9NN/FYCm/nDCIBr9t+fA 49 | 7Xz8QraxySzpROvmLZ4Z9hAdUDGMKSDuFWmV0R0s/g5g79KS7ErEX+EgbucNQ0nE 50 | shJXoc/ATxZscGj0SsXsq3X4oyEBapB4kWklcKLR+ClFDXPCZvHXoTjsgKZ2+xIA 51 | MFCRw8CzZzHNppjEG4yIh9pcwsKfTMXUdNCQV5NlNjh9yhVW8zQ5u6CngN4srL5j 52 | uMcRAgMBAAGjXTBbMB0GA1UdDgQWBBR6jrEu7TTPmZm6Xlehcj5kpZjbUjAfBgNV 53 | HSMEGDAWgBSzvS6sExvrnJVPn+rk305DiSx7nzAMBgNVHRMEBTADAQH/MAsGA1Ud 54 | DwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAiFAfxsqNk0yaiiL7XYpwLQ98YxMs 55 | DloaWRnLnR3H5kXstiaK3M2kED/AFYUX6ALLXPMZ+bjMzMymJkKehwAkhxN4Hc6l 56 | uJIY0xcf+Ynjsb+cDga/2QURqh1vMFw4F44RULLbHxMxxgLrDQjjsS0Z8pv7vGYf 57 | V+ekVX43sDb9n4G+/cFoffpENS8Wu42us84df98GpaGYreB5DUGATY6EgGy/0RON 58 | XMQ0DuXG7qgaLMkYpCY/s4q/C3T4tHlV54e1k4fsu2bicVdwKMYPaZRuYE6xq80p 59 | +tUY7vXJ/SMtUxlTLiHLHsw0fyTFmq7v1T3cMezghtWS07CDXbLSv4mbP36d4Lup 60 | Sp3u0JFNx7Cx2IU5kmWp15h5bDPRM2F063r3P6UC+/Lc2ImfP/ifZyEF2/gKtLNj 61 | Qq8K7NvattjEvxZkQHXKUfQe0jGMd2PXRjEuVSSnkCCueSNaPT8w33r0u3bX4d2T 62 | 158Y3QymyiIlUywuuIw31weV2cfPJY6x/L6VOuzlRl8OtNgeAmHGzlrkXTerBMYP 63 | 5OGzjkvfvY179LaxhJXprF+gRDqfz9P3svAuBdfC5Y2AXWRId8Lh/B9xU5MscU7r 64 | Hg8hbN1zR3jSkSWLS2hRjulXbNKLJdsUFwZvQtXsspQMbPndt3PROnT/w9NVuLUr 65 | 5XsvJAkpihAxKds= 66 | -----END CERTIFICATE----- 67 | -----BEGIN CERTIFICATE----- 68 | MIIFojCCA4qgAwIBAgIJAKNEPHLCT5cuMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 69 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 70 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 71 | MDIwOTAwNTIxMloXDTIyMDIwODAwNTIxMlowYTELMAkGA1UEBhMCVVMxCzAJBgNV 72 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 73 | YSBTb2x1dGlvbnMxEDAOBgNVBAMMB3Jvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUA 74 | A4ICDwAwggIKAoICAQDwEPViaE5R1tJWFccqL8R0lRgqI0E0dkOxpDahsmRowVGc 75 | k3oiOwN0AbxMa9Sb2kDsITv5IH4AG0hkt7UsEVsLiaJ1wLJTYzaAbHfzMTneYrpp 76 | aABTZaAhSdHdQ0uSwq2rKo/oyrrHcy7G+6wL+bxlv/EUkqrPH8gBaps/ZFTpD24h 77 | jANbKj3ZdUZqpqVh0TKiV6vrtHn/yRvldcYiuvonrBFXJMgFBRWK/MVmeQ5kn/Dj 78 | GVIjhQtD9AI9oyf7uAwm6s6NCJR6RiVRzkqw6UWM2+La/JUfZcp1oNjTolnwRtv1 79 | KyM77Fo/5TZYWiPFuYWtbBH7sPZfZUptka1/PPn8FZPfaL3mFNYWNj9/T1HCzXGp 80 | ICeI4fk+y9EanLcO54LcQEf5K9IVxQfBfA5D/ED+KVoJ82Q6LRJQcr2ZgP5HBcXn 81 | 76IXetboveXSKxNzNU8hMu+mCkm6RmMJ7ycaBoT2UOT5S++g+9zQzSmqkcWpky+D 82 | rZsBgOeNLVACjBwxBgL2E5WpuMyODrV32kPmVvhO/tHFmp07LvY4e0RLfxiexaTI 83 | nQbsBSqIusDScu0n2qaHoMN5nU+g/v+48uuF+l90BuxkOMn09k3K5glOsCQaBGEW 84 | bxhGhzdZZqjOwhbO2y5wFdondh/vMqtB+FY4gH9SZ5x856efBnRMYVou8GCZbQID 85 | AQABo10wWzAdBgNVHQ4EFgQUs70urBMb65yVT5/q5N9OQ4kse58wHwYDVR0jBBgw 86 | FoAUs70urBMb65yVT5/q5N9OQ4kse58wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMC 87 | AQYwDQYJKoZIhvcNAQELBQADggIBAAyHd5Mh3Pzyzy48TKJvYcna6HALwJV7CqhK 88 | g4Ff64qqquBhyjKIgC7WgdJw8hveApTP6g3uT5u0SC4BY/h+q0TacwFGZ6IMggHU 89 | TS8iBkBuagFXTtd0LFiigBvJVBv22IcY2tz/pPoS9eG2mMv/uaNLXx0Uor5DTvI1 90 | AbytUgQg0B/6ElDEZCG1Rvhs/ZOhx52extinw2X5HvVqPxMuwAUpo6Wp4vNn062l 91 | us8DVd4gW8t8RNZN5ktJ5XM4XZ2smG/wPDigpQ8OQtGKTZznrmv1+6O29F3cWXHF 92 | 4G3V7s2pi94ESW4VR8tzgH752+F2jBxp62H7foVacVLv3uADgqKP1YBGEuPJaaTG 93 | HsENxMxVKNJLssEV/+FOsr3me67qMyeWtI1bWHnyC6VvXAHMZrvTElVwqa2P9qEF 94 | II7UebrGSZUJZx+3WeoUC70a2B3N9bhFVaBbsMBqyg9K0Opj7BLXAL/GyFqj1Azj 95 | 7Ei0RydLw0PzreHBBkh/9Yq1Zc14nKlH/7STtRipjRoaHTrM/MVUd8yAHOSpPmUY 96 | KTJwWON+k2ZceUyApoN2ri5WaoacJjG/ct5tDIe3bfrK/wgArGENBSgWf07t+rMy 97 | 4K04KAwgxBoCoefeuQrvE8p8PUVneqNfUvV2DhdA0AD1lLUOV1OwfyRF9R52Zqof 98 | j14A3IDt 99 | -----END CERTIFICATE----- 100 | -------------------------------------------------------------------------------- /scripts/certs/client/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCiel/C4qikhR+l 3 | cuPuS3jfnUyenGU2S2v8slDvw+hUbY/MD6dcH2cnoj+OcSzdCvf10LhNSNrlr31T 4 | iEOAjGPBliRW3kQK1it0qAZlzTLYEBVeVQfuAg3qHgOOAqx1ujemZSAZr7O3h4Dv 5 | J9wO8JkfKcqPKi8c+h2t6QCgfdKqcDN6/1dxLyiTc9ruGkcMDChca+0E5XR/Ebpa 6 | MFpqKzo9fmMMNNkFX4w9wxB/BdVoRkdcUxpUPDa12rmorHYg7ayCUmTK7WV4ewco 7 | 4H1WICsJNT9OxaQCmSoTt11HHQ/lS1BAA74XP3AoK9r2CDl+CXX4BR7vK46rq+g8 8 | nPO3UdVgIQBOyFAso0DOzMQQOpomd8BxnaRE9cVlqIx0PaFpZzpT2U2NUYhYvw0O 9 | UKim/qqK5yfJIzgIQe/+yOsRVryWrKImOB7StHMZt5gl9FNe7FEp/FJSw7YkTBtc 10 | +M3vK7sUluS5sL4poLCb8b1BeEPFJmk3NiYpVacvoT9PKxamtl6CVR9GlrAkj8Al 11 | A0fbwLN3sSBRsfLLIFOeSI5REaEOqw3FiUMDbYCnBnlxef42o0j4tSshay3JJcqC 12 | WMPVtxzfwOQZ3W5L8r6PRvSWOdpjuazFESqnJpYLlrHNgXaTxSa+fUc+VJuz3Yg3 13 | 3bpftRcNW8OfYnFecDDU5+bjF2gu4wIDAQABAoICAEnOIPXcFBM3zZpQSC2U5wFc 14 | 6NMb4/X36MPPFGj1qvG/GxTs/28fbLJaR1ijHGgEdYKaHz4sHBM91kY9nbwlCaOE 15 | WlmDeZEanH7PXKidv7zaAXrqUUHSeROiMqctm72Wq3TQdvZ0E8UIro70oke/Dy9S 16 | woqp9ODK1PSM5RYCUdHOSfRTTRtC+sGQXSDPmvpkA2mINRCGUpbuLKKPWGLl4/zP 17 | Dpsa3oJMVt4vX6zzr29AjWXo8grVl1ygULnILZjjnftNBjAQConiCOyOnSCkWChE 18 | oc9chBYSEUUHhD2re+8CeR+7E3xn+sCku0ALmGIiVg4l9VlbheGBo2r1ONhLOgtJ 19 | Q4Oi9QzKbbtLXACiXae4K6meYWnqCWcBQ4lJyXPoZ7g30VIxjAcrEJw+y2f56mT7 20 | EdA/ft9XG5UUIm/GK35MpEqCA/yr5DIUShNtAMfQLDY3dAbhaowDuwsqEVNrlZnA 21 | NlEiSGJltZ62V6C/spCDeNxPG5rrRWuKqg4Kcku+ABF0impN/Uh+aAAIEgeyZTkQ 22 | auNIBaB5ISDGhxMdTEwUao58OU6+3H7sv79RAvwY/MucG/N7rQghUEuWrYmgEjTz 23 | K7kpnfu7i/W91VVCxsSw6EiNWU8rU7DCGLj/FAulZznlL3x1tY2TNHaqeituUWd4 24 | QAgMp9AnoaPwidgRMzURAoIBAQDUMZMlb9I4AlJyiiXrhRDeyQAy1lQh4BaSIzK9 25 | dALQoXL86ICG8CohqtAZZhOk1Gh8qbgbY/QK8L1DEKrGhKr0EvKcRLAbfJ/W+Fdm 26 | jOua4L2SUA2rVj6fwXd749BEEN5hgFbq4BuL4ELVm05igLNOzAW07C+kif7psxK7 27 | qDTgcE5d0MQwIHCO/d0cm/LkqQlJM8qfHYNYc3eWmPgqkQqtj/n6w5vIiQylP1tA 28 | T82vNd5QbV9hGOkn2jm1Ib1Uw4LSG6med7Q92qyi7g0Wp1fqrXhcIfNTXhv7+R/m 29 | 8GrTdGauq/Ly6Xko/mP2D6qlZtigSWmozH7016GGONt9Yz1tAoIBAQDEBVSc795R 30 | oYjP3sNeSIqm9EwqMquBVN3l+bf8tJnXUYQum41O+DZldMErp3Az9k4fPfk3cQaG 31 | ZQ1p7UkQC03Ev4CanArmJG5IDgjdXkE6UvntKabSbuHIs9HfQGAuxaHFDQF5yQhW 32 | EJ2Go+AAHW3Na7KqfIiNbJbBTYYyB6FaRlU5BfnXREz4I2FErZjZLYnBM641pv6y 33 | nyQ9HOguJiwxt8N72zPFr3Ccb4Wl5Ez0V5UdSQAuTBV59BgUU05kEgPH7PuCD+FJ 34 | MhfWaG/+CHv/URVQWuPE4n4y/AA+hSxjeX2fgVPm3T+85YOhoJQAZEJW3s0rYQj4 35 | LVS6W3RSW/uPAoIBAFrhhLlDBFFSxUcjXgxaaJhSlFAxtcTwehpLEv9Lga0jiOFD 36 | H1Er6Y+T3h8SthUhmELUWfL5Q1uwMg9YQxxVU/bHart9O8P6A2eJ470bXAlweiEB 37 | QmmAvb+kl+0q7FdKtkQiOJfONl4ZRebA5riPkLyN61/KzLj3FYO4W6yd5U+/KgFK 38 | WcyXCD5mAl6veD9xGnYMxZqShF/MzIpmp9trVZxyjslVIBKqiAz6brXDuD9znezq 39 | ezBV//4+m/q1Iab9ObmNeX9Sm5LmZow90sNCFwh/FY64R8QMCYZI893wWICKWTQ0 40 | lxBs6KSjF+MaYKcIvn0gFC3kr/djp12aWn+Akp0CggEBAL+zqt1UZOsWfDM224Aw 41 | l5DRqE84gKwaMG0B/GKiyTVfnlLqg5bwflpYhbZIqR3YSuyY8kq7ObNjzmuCyQps 42 | 06CP3JjX9HzAsi27tAWycT41+gBr1J9GG3/qBF0+mxIC68NHheLxrTpmQ3hyedYC 43 | 8eiM5A2BfxY4VXM0ewIgnjcoQ55h7sYNIO0nxK+iyrHSrD8LaNtFtx8/LfSqeZLw 44 | eBYjboWJiadSfWIwWtczX+MyaD1csFWWyQYcpxPxqN+LErGKXSoEl8G6wL9KIgSh 45 | jYrTZHHSgaCCFQONmjuKiC+8mBPPKhvE0tnM6B/AaB2973b/rE05N8lmo88hM7B8 46 | DpcCggEBAM7kTiQ6er4yuEXZ/TQv2wtsY+r4PAdEk8M0aCMlid9wKmS35kDBVYsI 47 | Mm2JwBzp2bPzgtGkb2QY+gImdfBz66Gtlc93c16GMmkI8zOb77SYcpPKuCSYvI5G 48 | wXq8IS7c8sVngxiHnudYl2p+8/m46KtliyrJUp29JPHgz9b/z/xQ70/ZhB+Pz+nZ 49 | vVZiJeDaKkmLqQbjgm6ajRTewdvxbQm1qYH6ENN1wKVuHroYxQAkPJLkn38thTCa 50 | HR0yANzearAauUV/hV5hVvVR+JQ2fMWexCXwYfaIRHELPcniHkzWu07mNuGn7B7U 51 | MB3G8hOps8uHFbwtQnIvaA8VQk9NrNs= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /scripts/certs/generate-client-cert.sh: -------------------------------------------------------------------------------- 1 | USERNAME=$1 2 | PASSPHRASE=$2 3 | 4 | # Create client certificate signing request. 5 | openssl req -nodes -new -newkey rsa:4096 -sha256 -keyout "$USERNAME.key" \ 6 | -out "$USERNAME.csr" \ 7 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=$USERNAME" \ 8 | -passin "pass:$PASSPHRASE" 9 | 10 | # Create client certificate by signing with intermediate client CA certificate. 11 | openssl x509 -extfile /etc/ssl/openssl.cnf -extensions usr_cert -req -days 1825 \ 12 | -CA client-intermediate.crt -CAkey client-intermediate.key \ 13 | -CAcreateserial -in "$USERNAME.csr" -out "$USERNAME.crt" \ 14 | -passin "pass:$PASSPHRASE" 15 | 16 | -------------------------------------------------------------------------------- /scripts/certs/generate-ssl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SSL_DIR="." 4 | 5 | DOMAIN="*.crunchy.lab" 6 | 7 | PASSPHRASE="password" 8 | 9 | SUBJ=" 10 | C=US 11 | ST=VA 12 | L=Arlington 13 | O=Crunchy Data Solutions 14 | CN=$DOMAIN 15 | " 16 | 17 | CA_CN=root-ca.crunchy.lab 18 | 19 | mkdir -p server client 20 | 21 | # ---- 22 | # Create self-signed CA 23 | # ---- 24 | 25 | # Create CA private key. 26 | openssl genrsa -aes256 -out ca.key -passout "pass:$PASSPHRASE" 4096 27 | 28 | # Create self-signed CA certificate. 29 | openssl req -new -x509 -sha256 -days 1825 -key ca.key -out ca.crt \ 30 | -passin "pass:$PASSPHRASE" \ 31 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=root-ca" 32 | 33 | # ---- 34 | # Create intermediate CAs 35 | # ---- 36 | 37 | # Create server intermediate private key. 38 | openssl genrsa -aes256 -out server-intermediate.key -passout "pass:$PASSPHRASE" 4096 39 | 40 | # Create server intermediate certificate signing request. 41 | openssl req -new -sha256 -days 1825 -key server-intermediate.key \ 42 | -out server-intermediate.csr \ 43 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=server-im-ca" \ 44 | -passin "pass:$PASSPHRASE" 45 | 46 | # Create server intermediate certificate by signing with CA certificate. 47 | openssl x509 -extfile /etc/ssl/openssl.cnf -extensions v3_ca -req -days 1825 \ 48 | -CA ca.crt -CAkey ca.key -CAcreateserial \ 49 | -in server-intermediate.csr -out server-intermediate.crt \ 50 | -passin "pass:$PASSPHRASE" 51 | 52 | # Create client intermediate private key. 53 | openssl genrsa -aes256 -out client-intermediate.key -passout "pass:$PASSPHRASE" 4096 54 | 55 | # Create client intermediate certificate signing request. 56 | openssl req -new -sha256 -days 1825 -key client-intermediate.key \ 57 | -out client-intermediate.csr \ 58 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=client-im-ca" \ 59 | -passin "pass:$PASSPHRASE" 60 | 61 | # Create client intermediate certificate by signing with CA certificate. 62 | openssl x509 -extfile /etc/ssl/openssl.cnf -extensions v3_ca -req -days 1825 \ 63 | -CA ca.crt -CAkey ca.key -CAcreateserial \ 64 | -in client-intermediate.csr -out client-intermediate.crt \ 65 | -passin "pass:$PASSPHRASE" 66 | 67 | # ---- 68 | # Create server/client certificates 69 | # ---- 70 | 71 | # Create server certificate signing request. 72 | openssl req -nodes -new -newkey rsa:4096 -sha256 -keyout server.key \ 73 | -out server.csr \ 74 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=$DOMAIN" \ 75 | -passin "pass:$PASSPHRASE" 76 | 77 | # Create server certificate by signing with intermediate server CA certificate. 78 | openssl x509 -extfile /etc/ssl/openssl.cnf -extensions usr_cert -req -days 1825 \ 79 | -CA server-intermediate.crt -CAkey server-intermediate.key \ 80 | -CAcreateserial -in server.csr -out server.crt \ 81 | -passin "pass:$PASSPHRASE" 82 | 83 | # Create client certificate signing request. 84 | openssl req -nodes -new -newkey rsa:4096 -sha256 -keyout client.key \ 85 | -out client.csr \ 86 | -subj "/C=US/ST=VA/L=Arlington/O=Crunchy Data Solutions/CN=postgres" \ 87 | -passin "pass:$PASSPHRASE" 88 | 89 | # Create client certificate by signing with intermediate client CA certificate. 90 | openssl x509 -extfile /etc/ssl/openssl.cnf -extensions usr_cert -req -days 1825 \ 91 | -CA client-intermediate.crt -CAkey client-intermediate.key \ 92 | -CAcreateserial -in client.csr -out client.crt \ 93 | -passin "pass:$PASSPHRASE" 94 | 95 | cp ca.crt server/ca.crt 96 | cp server.key server/server.key 97 | cat server.crt server-intermediate.crt ca.crt > server/server.crt 98 | chmod 600 server/* 99 | 100 | cp ca.crt client/ca.crt 101 | cp client.key client/client.key 102 | cat client.crt client-intermediate.crt ca.crt > client/client.crt 103 | chmod 600 client/* 104 | 105 | -------------------------------------------------------------------------------- /scripts/certs/server/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFojCCA4qgAwIBAgIJAKNEPHLCT5cuMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 4 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 5 | MDIwOTAwNTIxMloXDTIyMDIwODAwNTIxMlowYTELMAkGA1UEBhMCVVMxCzAJBgNV 6 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 7 | YSBTb2x1dGlvbnMxEDAOBgNVBAMMB3Jvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUA 8 | A4ICDwAwggIKAoICAQDwEPViaE5R1tJWFccqL8R0lRgqI0E0dkOxpDahsmRowVGc 9 | k3oiOwN0AbxMa9Sb2kDsITv5IH4AG0hkt7UsEVsLiaJ1wLJTYzaAbHfzMTneYrpp 10 | aABTZaAhSdHdQ0uSwq2rKo/oyrrHcy7G+6wL+bxlv/EUkqrPH8gBaps/ZFTpD24h 11 | jANbKj3ZdUZqpqVh0TKiV6vrtHn/yRvldcYiuvonrBFXJMgFBRWK/MVmeQ5kn/Dj 12 | GVIjhQtD9AI9oyf7uAwm6s6NCJR6RiVRzkqw6UWM2+La/JUfZcp1oNjTolnwRtv1 13 | KyM77Fo/5TZYWiPFuYWtbBH7sPZfZUptka1/PPn8FZPfaL3mFNYWNj9/T1HCzXGp 14 | ICeI4fk+y9EanLcO54LcQEf5K9IVxQfBfA5D/ED+KVoJ82Q6LRJQcr2ZgP5HBcXn 15 | 76IXetboveXSKxNzNU8hMu+mCkm6RmMJ7ycaBoT2UOT5S++g+9zQzSmqkcWpky+D 16 | rZsBgOeNLVACjBwxBgL2E5WpuMyODrV32kPmVvhO/tHFmp07LvY4e0RLfxiexaTI 17 | nQbsBSqIusDScu0n2qaHoMN5nU+g/v+48uuF+l90BuxkOMn09k3K5glOsCQaBGEW 18 | bxhGhzdZZqjOwhbO2y5wFdondh/vMqtB+FY4gH9SZ5x856efBnRMYVou8GCZbQID 19 | AQABo10wWzAdBgNVHQ4EFgQUs70urBMb65yVT5/q5N9OQ4kse58wHwYDVR0jBBgw 20 | FoAUs70urBMb65yVT5/q5N9OQ4kse58wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMC 21 | AQYwDQYJKoZIhvcNAQELBQADggIBAAyHd5Mh3Pzyzy48TKJvYcna6HALwJV7CqhK 22 | g4Ff64qqquBhyjKIgC7WgdJw8hveApTP6g3uT5u0SC4BY/h+q0TacwFGZ6IMggHU 23 | TS8iBkBuagFXTtd0LFiigBvJVBv22IcY2tz/pPoS9eG2mMv/uaNLXx0Uor5DTvI1 24 | AbytUgQg0B/6ElDEZCG1Rvhs/ZOhx52extinw2X5HvVqPxMuwAUpo6Wp4vNn062l 25 | us8DVd4gW8t8RNZN5ktJ5XM4XZ2smG/wPDigpQ8OQtGKTZznrmv1+6O29F3cWXHF 26 | 4G3V7s2pi94ESW4VR8tzgH752+F2jBxp62H7foVacVLv3uADgqKP1YBGEuPJaaTG 27 | HsENxMxVKNJLssEV/+FOsr3me67qMyeWtI1bWHnyC6VvXAHMZrvTElVwqa2P9qEF 28 | II7UebrGSZUJZx+3WeoUC70a2B3N9bhFVaBbsMBqyg9K0Opj7BLXAL/GyFqj1Azj 29 | 7Ei0RydLw0PzreHBBkh/9Yq1Zc14nKlH/7STtRipjRoaHTrM/MVUd8yAHOSpPmUY 30 | KTJwWON+k2ZceUyApoN2ri5WaoacJjG/ct5tDIe3bfrK/wgArGENBSgWf07t+rMy 31 | 4K04KAwgxBoCoefeuQrvE8p8PUVneqNfUvV2DhdA0AD1lLUOV1OwfyRF9R52Zqof 32 | j14A3IDt 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /scripts/certs/server/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFyzCCA7OgAwIBAgIJANqdFYGJijneMA0GCSqGSIb3DQEBCwUAMGYxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 4 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRUwEwYDVQQDDAxzZXJ2ZXItaW0tY2Ew 5 | HhcNMTcwMjA5MDA1MjE0WhcNMjIwMjA4MDA1MjE0WjBnMQswCQYDVQQGEwJVUzEL 6 | MAkGA1UECAwCVkExEjAQBgNVBAcMCUFybGluZ3RvbjEfMB0GA1UECgwWQ3J1bmNo 7 | eSBEYXRhIFNvbHV0aW9uczEWMBQGA1UEAwwNKi5jcnVuY2h5LmxhYjCCAiIwDQYJ 8 | KoZIhvcNAQEBBQADggIPADCCAgoCggIBALA51YrPGVEw/10FkJSTTcZwXaM3ybGt 9 | nDTvV5HAU5Esg41IWfwYe91o83Mtc+gmxGAxoAwORxS1wzEvHYqLi7alvBQxM0kd 10 | lyYpdm9QAcTOP9XShy0ImhBqUv6NUN/ELHf6XZpgRt5xz+fWHiI/UxNwGq2zytNv 11 | x5/8iS8/0QWyykGnf7F3Vj0Z50VbB8NulX8TS5KlUelw3PTYg6xysZdHYEU7W2H9 12 | w7LSODHH/XWMbArYvhnXK4E5VT0FTzt1P4rSW3NSVLptQwZA8YU5k0PbIpo3eyRM 13 | iH9EnzqP5pthn6n2A44fO+oaIh/SB5g8YQUXiy2M3gmVA5B6Kq18EoKxCRd/JD0C 14 | FhLwo3PlEwIenSRsfeVUSGlCNwkG2h9aRwcwlH/Yzy6r6Bb1JL2iKmEtgFHxs99f 15 | 78tllxQuDVPwELbw5Kv8/Dp7BR1R+iG85scmGbEUaEen1RiAbAlixCm9Jlmq68is 16 | Q5gZLaZj43Mkap8tUTvGFjFJMEFfdyCuP277rXaNlYuMm2g90xjBZZ6t9+Qk533w 17 | vfFa9eVhMRtc58gK82sHEwGCMUCeyVF31lN2mR8xNSrzkyYmU2mftQo4wQkMTjvv 18 | FWcZMiYAXrsk6kc+u6YZVPGbIS/Jub172OctYTD3fbS102t1kHq1ZSIstGbNQI2P 19 | 4uem7nu8Izb3AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9w 20 | ZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRCidyGEKdervT6 21 | L99SB9KzGkXGFDAfBgNVHSMEGDAWgBSlSrB1uahqFsDJSmWsdQbaYnnozTANBgkq 22 | hkiG9w0BAQsFAAOCAgEAeky6sGYZtA3+fMj4cl43Cq4QJAmo4VqZtexC9f3MPpaG 23 | HuPRDzEph69nPeQfFofB175vHrQgjRe+NZ42EYrKiXBJhiQygYXiOt2WTuuinI2S 24 | AcSh7RdttodRoKcVj6THnNmnRp1Q5heVmI4xylwl0dflVBg02S0FG9Cs0tikXpRL 25 | 0312vIv4ZrJYxgTm7iZVfj/OoxfLIYvU9F3y5R5hsAd7eZZLfMEYk6DtgDcQhh5v 26 | xVFVOW5VUNwWLYw6OPoyMZBQkTsiNliYx/KuI88alC9Y37KXXPwFnQgl5O2F4CFF 27 | adXmZRGYQOGpu43o99K43iPTlalmrB1ijtFV7TpdAWflTEpiqzI6I09JnucG5pEF 28 | EpLvUUE16rCtQpzZyRHFqMNBm5tA5NR9L6Y49vohJ0MJrxnMZFuuLt2Kx4WqJpHH 29 | wbfe90rsoLWjhkThJHpxzPMQO1L+GKe3yb5vTYNGsqn4BeBosS5xndnug2KjvjCA 30 | yOyU7vOMZfRzPDvoFW/tUZMfrK/U5+iafxDGeNogheZyCuFaLlDz8pckA/dutViw 31 | tiU/ciGYD3+fAOYcuzegVXYfnJHTYrZwdwJYcvYwWcihtRjdEpjH+iLkMy0eb/XK 32 | sa19bqh+Vq2vkYufpIMm1cpgrW2jKqz/9QHTz3a2BBfQZTsILDnRpAp3wX8laAo= 33 | -----END CERTIFICATE----- 34 | -----BEGIN CERTIFICATE----- 35 | MIIFpzCCA4+gAwIBAgIJAPfdFYztRKeOMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 36 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 37 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 38 | MDIwOTAwNTIxMloXDTIyMDIwODAwNTIxMlowZjELMAkGA1UEBhMCVVMxCzAJBgNV 39 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 40 | YSBTb2x1dGlvbnMxFTATBgNVBAMMDHNlcnZlci1pbS1jYTCCAiIwDQYJKoZIhvcN 41 | AQEBBQADggIPADCCAgoCggIBAM/YPjonAfcoUsY/oL+7dKHwn36zwYKg1hlHhFym 42 | +t/hOKZ6jvp6D3K6UKc/FpzSENhJ1szuohvxHNbJcnkbbSVuv6LQpXbT4PNebJr1 43 | 4fn1zLX0v+LXqRP3qumrgatoicMf1M7op0taSlRhtbRn079x3X5KXWok/cbJ3PqN 44 | 3khPfnZCbcufm7PUpVpS5AQtA/nbt2zysg8iMXvGCFtP6ybnq7J+WfsfL8o/I1Vr 45 | KdkDEUsXrYgougeLxtBfFwruajGi8n5gu5DAgdHtMUnrdvrIttZ2dnK9i/2KTZpV 46 | MEbG6yRAagoutgzna+KlxveyXQFh55Wh0768I0QqkxQWGh5HLmon0EZ9bObuRkv/ 47 | WGHGHMepA74aIDiNnWOxyYDlVSn+jmTpNcUE6rnEadVsJ8UVcdVcmcqHTxlbCMuf 48 | j3hstX4l/XRAvDMa/6nJET5BNKi5hGpNdlSQtboh7aF6jbsHzGrfQWZYM/iDEVuC 49 | rVhZovFCR8ik8jrzY4+9fJseuU8C8NKvLLZJXEDdywCfoGFhKnOlg5N3inYwBScH 50 | B8y7BfZGFPUu9HOaZ8pHd4uuw402HiocospW6M42qNscgzLymOpP9/0K9oGOSpbX 51 | YQUIko3UGHH/eNblHXnbEz6b14dCj2Rk1MqERv7/F/ontMC1wJYi9pJOZLwjomwh 52 | DuqNAgMBAAGjXTBbMB0GA1UdDgQWBBSlSrB1uahqFsDJSmWsdQbaYnnozTAfBgNV 53 | HSMEGDAWgBSzvS6sExvrnJVPn+rk305DiSx7nzAMBgNVHRMEBTADAQH/MAsGA1Ud 54 | DwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAtykE7qTjH6EByHqPiTr0782lxNPD 55 | zVIBe/XrBtdnnE1iuv/Wb33Kd6EfuX85I8GSy2Gs2euyxiILppxIzxI8dewMpWIn 56 | ns+fEUYFA7JuhFg/GkIVlbe0NAjUXvzaPHMxjz6YToj55pMLuZ0IC+hYZkUpibtE 57 | KII7FH1xzMQ5aiamW2eAs1VR7sjrt4DKfLaD026KfMvo4q6WOd2n4QwCDfSA19AB 58 | vGx+o3DnavVCP/pkzDO6RC5aEirgIPtwz3Tuf7TJfofuksP22ySNzRYKY6+8dmwn 59 | GSESd809OupyUp7X7/XDi4QAemJ72Q46iofAUZuMWaOr9FUGsZkO+TP77aSye3+Z 60 | N0ifA9RKuFwAojWI5caftpLd2U2pXR/YrH07h8RcKwZCbX0BmUnBZ5zjQ/ylWy/a 61 | o0MZ7I3C0C/1VXksKkxaiVuWHxdG33rt5J2MeyanNoTMypJfMKMOsZUcBZlQe81V 62 | G/rD3GklVZdeEUWPtCWTGpP+XkEmiPECe63ENuEAzeEnkJAnx76Be8qnqZ3CK0uc 63 | PkEBZn2gDRZNMbze0Dm4/1qWLc56iPWvxlGvqqYYESetnqxQv2zGuVSwXrhoWyx8 64 | 3hDfe4LjHIuC8zZ1TN0BhSIXRUxL00sCdnNqXr/UvoRc+rBfeRo3QE8KlSZY7aGz 65 | oPE1vy5oy6I7e4M= 66 | -----END CERTIFICATE----- 67 | -----BEGIN CERTIFICATE----- 68 | MIIFojCCA4qgAwIBAgIJAKNEPHLCT5cuMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 69 | BAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJQXJsaW5ndG9uMR8wHQYDVQQK 70 | DBZDcnVuY2h5IERhdGEgU29sdXRpb25zMRAwDgYDVQQDDAdyb290LWNhMB4XDTE3 71 | MDIwOTAwNTIxMloXDTIyMDIwODAwNTIxMlowYTELMAkGA1UEBhMCVVMxCzAJBgNV 72 | BAgMAlZBMRIwEAYDVQQHDAlBcmxpbmd0b24xHzAdBgNVBAoMFkNydW5jaHkgRGF0 73 | YSBTb2x1dGlvbnMxEDAOBgNVBAMMB3Jvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUA 74 | A4ICDwAwggIKAoICAQDwEPViaE5R1tJWFccqL8R0lRgqI0E0dkOxpDahsmRowVGc 75 | k3oiOwN0AbxMa9Sb2kDsITv5IH4AG0hkt7UsEVsLiaJ1wLJTYzaAbHfzMTneYrpp 76 | aABTZaAhSdHdQ0uSwq2rKo/oyrrHcy7G+6wL+bxlv/EUkqrPH8gBaps/ZFTpD24h 77 | jANbKj3ZdUZqpqVh0TKiV6vrtHn/yRvldcYiuvonrBFXJMgFBRWK/MVmeQ5kn/Dj 78 | GVIjhQtD9AI9oyf7uAwm6s6NCJR6RiVRzkqw6UWM2+La/JUfZcp1oNjTolnwRtv1 79 | KyM77Fo/5TZYWiPFuYWtbBH7sPZfZUptka1/PPn8FZPfaL3mFNYWNj9/T1HCzXGp 80 | ICeI4fk+y9EanLcO54LcQEf5K9IVxQfBfA5D/ED+KVoJ82Q6LRJQcr2ZgP5HBcXn 81 | 76IXetboveXSKxNzNU8hMu+mCkm6RmMJ7ycaBoT2UOT5S++g+9zQzSmqkcWpky+D 82 | rZsBgOeNLVACjBwxBgL2E5WpuMyODrV32kPmVvhO/tHFmp07LvY4e0RLfxiexaTI 83 | nQbsBSqIusDScu0n2qaHoMN5nU+g/v+48uuF+l90BuxkOMn09k3K5glOsCQaBGEW 84 | bxhGhzdZZqjOwhbO2y5wFdondh/vMqtB+FY4gH9SZ5x856efBnRMYVou8GCZbQID 85 | AQABo10wWzAdBgNVHQ4EFgQUs70urBMb65yVT5/q5N9OQ4kse58wHwYDVR0jBBgw 86 | FoAUs70urBMb65yVT5/q5N9OQ4kse58wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMC 87 | AQYwDQYJKoZIhvcNAQELBQADggIBAAyHd5Mh3Pzyzy48TKJvYcna6HALwJV7CqhK 88 | g4Ff64qqquBhyjKIgC7WgdJw8hveApTP6g3uT5u0SC4BY/h+q0TacwFGZ6IMggHU 89 | TS8iBkBuagFXTtd0LFiigBvJVBv22IcY2tz/pPoS9eG2mMv/uaNLXx0Uor5DTvI1 90 | AbytUgQg0B/6ElDEZCG1Rvhs/ZOhx52extinw2X5HvVqPxMuwAUpo6Wp4vNn062l 91 | us8DVd4gW8t8RNZN5ktJ5XM4XZ2smG/wPDigpQ8OQtGKTZznrmv1+6O29F3cWXHF 92 | 4G3V7s2pi94ESW4VR8tzgH752+F2jBxp62H7foVacVLv3uADgqKP1YBGEuPJaaTG 93 | HsENxMxVKNJLssEV/+FOsr3me67qMyeWtI1bWHnyC6VvXAHMZrvTElVwqa2P9qEF 94 | II7UebrGSZUJZx+3WeoUC70a2B3N9bhFVaBbsMBqyg9K0Opj7BLXAL/GyFqj1Azj 95 | 7Ei0RydLw0PzreHBBkh/9Yq1Zc14nKlH/7STtRipjRoaHTrM/MVUd8yAHOSpPmUY 96 | KTJwWON+k2ZceUyApoN2ri5WaoacJjG/ct5tDIe3bfrK/wgArGENBSgWf07t+rMy 97 | 4K04KAwgxBoCoefeuQrvE8p8PUVneqNfUvV2DhdA0AD1lLUOV1OwfyRF9R52Zqof 98 | j14A3IDt 99 | -----END CERTIFICATE----- 100 | -------------------------------------------------------------------------------- /scripts/certs/server/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCwOdWKzxlRMP9d 3 | BZCUk03GcF2jN8mxrZw071eRwFORLIONSFn8GHvdaPNzLXPoJsRgMaAMDkcUtcMx 4 | Lx2Ki4u2pbwUMTNJHZcmKXZvUAHEzj/V0octCJoQalL+jVDfxCx3+l2aYEbecc/n 5 | 1h4iP1MTcBqts8rTb8ef/IkvP9EFsspBp3+xd1Y9GedFWwfDbpV/E0uSpVHpcNz0 6 | 2IOscrGXR2BFO1th/cOy0jgxx/11jGwK2L4Z1yuBOVU9BU87dT+K0ltzUlS6bUMG 7 | QPGFOZND2yKaN3skTIh/RJ86j+abYZ+p9gOOHzvqGiIf0geYPGEFF4stjN4JlQOQ 8 | eiqtfBKCsQkXfyQ9AhYS8KNz5RMCHp0kbH3lVEhpQjcJBtofWkcHMJR/2M8uq+gW 9 | 9SS9oiphLYBR8bPfX+/LZZcULg1T8BC28OSr/Pw6ewUdUfohvObHJhmxFGhHp9UY 10 | gGwJYsQpvSZZquvIrEOYGS2mY+NzJGqfLVE7xhYxSTBBX3cgrj9u+612jZWLjJto 11 | PdMYwWWerffkJOd98L3xWvXlYTEbXOfICvNrBxMBgjFAnslRd9ZTdpkfMTUq85Mm 12 | JlNpn7UKOMEJDE477xVnGTImAF67JOpHPrumGVTxmyEvybm9e9jnLWEw9320tdNr 13 | dZB6tWUiLLRmzUCNj+Lnpu57vCM29wIDAQABAoICABsYuXH/iBgI3asS+PkvcS0L 14 | CArZwCSW2kb1alSZ6v+OhafKsTNnHFyeD5jtKz5N/7H+z2orI9jWXe4JHZS5AkGS 15 | WEoOiqE6cIxIodJZzfvR7tTU6z4MbYwKnTpQxlex1ViZYZ0kIqF/hIVUUgvYCtEZ 16 | UrDUzWIXdO1vqjRerqgepqTTuXmzLMp2Wi464gW10jzY3aztyJuJZCAtzolMlWv6 17 | aJ4ck1+M4nNziXp2Es3pSo+cZhaZFDJpe2bHkzhEPgH9eFxSE38a38xtXfWrNa6g 18 | WkC2OTQKpI5VIdqiV8Yai69ZiZBn0xGa9RZJeCt7VTuwNBFL8LQuen7Tmg5U3o+C 19 | hGu5PDZgdYwFFxflW2Bk+r/guAT+4/BlEu8Eo4Xf3ulaBufOM/Ta2vYfTzLHbte/ 20 | EAEvWaCU+xkjINBHvSPoyOQCsV+U6sQioRRp3mhCNluB3AlcWAp6btDwWNUfDFen 21 | EVdnX9RUciBlOQq3yzz2n20EVsIr4njsN6k+EfKbs8eMvfMg9pQUO1jntRuMWopv 22 | AAVAdhXswoVb94ZDz8xYn2YgmcFLKIPHbkdmUqdx65io1Yhm1yR8UeZ+Ttln+INh 23 | j/d0t0YxuvJVi93jF7eiDJYeyhGxcePWlyDYA7K5DIQuqTpmIrqammd8X3EVVvWG 24 | UQ/F/TcyEi61IC2dmxyZAoIBAQDdBRU+o2XftcaP5V+zGiNqUZJdcbQeFhI5oFYY 25 | 7zPV1dCjCstT53Q2uBWYji7s6BH6RxcLts9N7Guyb4rlbtdBsvmeKmtMTo13KkWq 26 | kPCkKMtHmav12sAxcciFjDbQoq4/7u99OP36uNl6hwCgJnXDTwbBEneDIlMSphwu 27 | dO9aB6XTtu24qN+PAXq0va90AXY0otdAnU3Armx2j6nMtiF6FhDu4aTAtUxo8ExL 28 | n5QPQVmfsHOygY7r7E7dfLEbl/WSgO6y/Go7lGcOeIz/kOf+RJDttnsfkDit5wKe 29 | dls/qobSXy3k7SUT0/nSi5PaMrzHyjxM3YwTZ6mG/ZctiRZDAoIBAQDMHd1FnHb8 30 | rZD6nS45J5hMt8V9orHwIgxrqE1FmY/mLag9M+S1Yf+JfYPBaPRPPp8F6JCakVMS 31 | Wf/x547ByY5w0QEjVq+ZK6elCkv035kwJjmlqBo/AmtT6B364U87sXtnCeDMfsvn 32 | 69GdA5QJWO2U0jyXijrouavlFA3Tg/3rXTm1rnCso/0jFlrmZMSlDzyUmuFwIeMW 33 | 36D6H4BD0eFpMIFhHx1P/8n7gPPNLc97Ws+BPYo7KbTJlDlql6yhorLkM+8K4LIZ 34 | F6wEXmkKarqxRgJxonWmyGyZhUlDOy9OQ1RCLWazA/rUtGwntUXZSlsZMdrbgzbA 35 | gTfkkuc0X2M9AoIBAEMdcQcVXTEipMo7b+UWMdzuKlD/3x8nup8PJjXqJUZw6Csq 36 | cvXmme6n1b7+phdAnXzvq3reJ3NVMJr9riLflrQXoh86aFzCmciu6HrUCo7+6EO0 37 | 0XgV1jjtgWO0YUzXqaw2ZTkUXmONUFUZu8HmC+qkC7wDXJFQkgNet74PukMPgXaZ 38 | 3PQvK7vkShuqYiYK75cqP80aN2skTEFwfLJyOBY3ryqrEHmIVEylc7WfHrnHMW0B 39 | ixCQsRLJLmWpoKptaZ/0UQe/CI9RED5fswlEOn6G2a+lsxuxktSUCsKN1EWwBcLK 40 | LkiPDqxHuVwLEHm44ISWz9rTh964os0gIpEsSLsCggEAe9YulfosknZ3PCU6kwB7 41 | vSfNkMCxdgAiSvqsDurwy/StWpxuSMFw+KiG1EPp3V4fusTzXmOG495okwoqqVWw 42 | rq+PIjKIjMRYjJ/nn2Q9G2JFnUSUz9+W4Cw2bCxgFtZCCSyMD2VqLoX7AWjXWqhy 43 | U9UNX99EiEeFi5EUJlyxJCfGSHzR7Vv8y1nXwmomiAHrfjlNRuu4S/tXPgj4l6zx 44 | NhFwnUz0SsHkeEsvHy+I7s0UkdOVVvmhrp7YCtyEpvf/0sXTZGUYiHK9MVVaIzDH 45 | nZ/dAbZP7OUyRh4AaeKxe6kLqK7E3R0wA/aGm7y16PPcuzdtEbrU8Aw1rd6WmUa5 46 | /QKCAQAM32J0Bw3kXe7lLpO0Yx9GojGBtqvWXWjz4JUYpk3oXs+M4yPg9b+RR7wt 47 | lH0mx6OgfWL0wzAZCIEuvne71+efQyut/RUTaiCqISClt3VoZuBngaEJndx8tioJ 48 | doAJ8Q3gVlKPpI7/oPqLbDdwrNcsd86qz5Cm+kHtzLqLFweLAVbd8uA9Dc3x38xX 49 | LXezr9zZPnffSqkRgpjC9oli5ejNXPK8QhBjbOEfZezRkB5qyTkiMcKMARqH8sVw 50 | K6IHatLYiCUu4JLWw2DtLZTnPZW2Tvj5wuEqBfjFyanVegnJrlBRg7mcbwteExvK 51 | fv0hkf1PrE+RL5qRqke1oSZHoyYu 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /scripts/docker/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | proxy: 3 | hostport: 0.0.0.0:5432 4 | admin: 5 | hostport: 0.0.0.0:10000 6 | 7 | nodes: 8 | master: 9 | hostport: 192.168.56.100:5432 10 | role: master 11 | metadata: {} 12 | replica: 13 | hostport: 192.168.56.101:5432 14 | role: replica 15 | metadata: {} 16 | 17 | credentials: 18 | username: postgres 19 | password: password 20 | database: postgres 21 | ssl: 22 | enable: false 23 | sslmode: disable 24 | options: 25 | application_name: crunchy-proxy 26 | 27 | pool: 28 | capacity: 2 29 | 30 | healthcheck: 31 | delay: 60 32 | query: select now() 33 | 34 | -------------------------------------------------------------------------------- /scripts/docker/master-config/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # crunchy-proxy PostgreSQL Client Authentication Configuration File 2 | # =================================================== 3 | # 4 | # Refer to the "Client Authentication" section in the PostgreSQL 5 | # documentation for a complete description of this file. A short 6 | # synopsis follows. 7 | # 8 | # This file controls: which hosts are allowed to connect, how clients 9 | # are authenticated, which PostgreSQL user names they can use, which 10 | # databases they can access. Records take one of these forms: 11 | # 12 | # local DATABASE USER METHOD [OPTIONS] 13 | # host DATABASE USER ADDRESS METHOD [OPTIONS] 14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS] 15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] 16 | # 17 | # (The uppercase items must be replaced by actual values.) 18 | # 19 | # The first field is the connection type: "local" is a Unix-domain 20 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket, 21 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a 22 | # plain TCP/IP socket. 23 | # 24 | # DATABASE can be "all", "sameuser", "samerole", "replication", a 25 | # database name, or a comma-separated list thereof. The "all" 26 | # keyword does not match "replication". Access to replication 27 | # must be enabled in a separate record (see example below). 28 | # 29 | # USER can be "all", a user name, a group name prefixed with "+", or a 30 | # comma-separated list thereof. In both the DATABASE and USER fields 31 | # you can also write a file name prefixed with "@" to include names 32 | # from a separate file. 33 | # 34 | # ADDRESS specifies the set of hosts the record matches. It can be a 35 | # host name, or it is made up of an IP address and a CIDR mask that is 36 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that 37 | # specifies the number of significant bits in the mask. A host name 38 | # that starts with a dot (.) matches a suffix of the actual host name. 39 | # Alternatively, you can write an IP address and netmask in separate 40 | # columns to specify the set of hosts. Instead of a CIDR-address, you 41 | # can write "samehost" to match any of the server's own IP addresses, 42 | # or "samenet" to match any address in any subnet that the server is 43 | # directly connected to. 44 | # 45 | # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", 46 | # "krb5", "ident", "peer", "pam", "ldap", "radius" or "cert". Note that 47 | # "password" sends passwords in clear text; "md5" is preferred since 48 | # it sends encrypted passwords. 49 | # 50 | # OPTIONS are a set of options for the authentication in the format 51 | # NAME=VALUE. The available options depend on the different 52 | # authentication methods -- refer to the "Client Authentication" 53 | # section in the documentation for a list of which options are 54 | # available for which authentication methods. 55 | # 56 | # Database and user names containing spaces, commas, quotes and other 57 | # special characters must be quoted. Quoting one of the keywords 58 | # "all", "sameuser", "samerole" or "replication" makes the name lose 59 | # its special character, and just match a database or username with 60 | # that name. 61 | # 62 | # This file is read on server startup and when the postmaster receives 63 | # a SIGHUP signal. If you edit the file on a running system, you have 64 | # to SIGHUP the postmaster for the changes to take effect. You can 65 | # use "pg_ctl reload" to do that. 66 | 67 | # Put your actual configuration here 68 | # ---------------------------------- 69 | # 70 | # If you want to allow non-local connections, you need to add more 71 | # "host" records. In that case you will also need to make PostgreSQL 72 | # listen on a non-local interface via the listen_addresses 73 | # configuration parameter, or via the -i or -h command line switches. 74 | 75 | # CAUTION: Configuring the system for local "trust" authentication 76 | # allows any local user to connect as any PostgreSQL user, including 77 | # the database superuser. If you do not trust all your local users, 78 | # use another authentication method. 79 | 80 | 81 | # TYPE DATABASE USER ADDRESS METHOD 82 | 83 | # "local" is for Unix domain socket connections only 84 | local all all trust 85 | # IPv4 local connections: 86 | host all all 127.0.0.1/32 trust 87 | host all all 0.0.0.0/0 md5 88 | host replication masteruser 0.0.0.0/0 md5 89 | # IPv6 local connections: 90 | #host all all ::1/128 trust 91 | # Allow replication connections from localhost, by a user with the 92 | # replication privilege. 93 | #local replication postgres trust 94 | #host replication postgres 127.0.0.1/32 trust 95 | #host replication postgres ::1/128 trust 96 | -------------------------------------------------------------------------------- /scripts/docker/replica-config/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # crunchy-proxy replica PostgreSQL Client Authentication Configuration File 2 | # =================================================== 3 | # 4 | # Refer to the "Client Authentication" section in the PostgreSQL 5 | # documentation for a complete description of this file. A short 6 | # synopsis follows. 7 | # 8 | # This file controls: which hosts are allowed to connect, how clients 9 | # are authenticated, which PostgreSQL user names they can use, which 10 | # databases they can access. Records take one of these forms: 11 | # 12 | # local DATABASE USER METHOD [OPTIONS] 13 | # host DATABASE USER ADDRESS METHOD [OPTIONS] 14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS] 15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] 16 | # 17 | # (The uppercase items must be replaced by actual values.) 18 | # 19 | # The first field is the connection type: "local" is a Unix-domain 20 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket, 21 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a 22 | # plain TCP/IP socket. 23 | # 24 | # DATABASE can be "all", "sameuser", "samerole", "replication", a 25 | # database name, or a comma-separated list thereof. The "all" 26 | # keyword does not match "replication". Access to replication 27 | # must be enabled in a separate record (see example below). 28 | # 29 | # USER can be "all", a user name, a group name prefixed with "+", or a 30 | # comma-separated list thereof. In both the DATABASE and USER fields 31 | # you can also write a file name prefixed with "@" to include names 32 | # from a separate file. 33 | # 34 | # ADDRESS specifies the set of hosts the record matches. It can be a 35 | # host name, or it is made up of an IP address and a CIDR mask that is 36 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that 37 | # specifies the number of significant bits in the mask. A host name 38 | # that starts with a dot (.) matches a suffix of the actual host name. 39 | # Alternatively, you can write an IP address and netmask in separate 40 | # columns to specify the set of hosts. Instead of a CIDR-address, you 41 | # can write "samehost" to match any of the server's own IP addresses, 42 | # or "samenet" to match any address in any subnet that the server is 43 | # directly connected to. 44 | # 45 | # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", 46 | # "krb5", "ident", "peer", "pam", "ldap", "radius" or "cert". Note that 47 | # "password" sends passwords in clear text; "md5" is preferred since 48 | # it sends encrypted passwords. 49 | # 50 | # OPTIONS are a set of options for the authentication in the format 51 | # NAME=VALUE. The available options depend on the different 52 | # authentication methods -- refer to the "Client Authentication" 53 | # section in the documentation for a list of which options are 54 | # available for which authentication methods. 55 | # 56 | # Database and user names containing spaces, commas, quotes and other 57 | # special characters must be quoted. Quoting one of the keywords 58 | # "all", "sameuser", "samerole" or "replication" makes the name lose 59 | # its special character, and just match a database or username with 60 | # that name. 61 | # 62 | # This file is read on server startup and when the postmaster receives 63 | # a SIGHUP signal. If you edit the file on a running system, you have 64 | # to SIGHUP the postmaster for the changes to take effect. You can 65 | # use "pg_ctl reload" to do that. 66 | 67 | # Put your actual configuration here 68 | # ---------------------------------- 69 | # 70 | # If you want to allow non-local connections, you need to add more 71 | # "host" records. In that case you will also need to make PostgreSQL 72 | # listen on a non-local interface via the listen_addresses 73 | # configuration parameter, or via the -i or -h command line switches. 74 | 75 | # CAUTION: Configuring the system for local "trust" authentication 76 | # allows any local user to connect as any PostgreSQL user, including 77 | # the database superuser. If you do not trust all your local users, 78 | # use another authentication method. 79 | 80 | 81 | # TYPE DATABASE USER ADDRESS METHOD 82 | 83 | # "local" is for Unix domain socket connections only 84 | local all all trust 85 | # IPv4 local connections: 86 | host all all 127.0.0.1/32 trust 87 | host all all 0.0.0.0/0 md5 88 | host replication masteruser 0.0.0.0/0 md5 89 | # IPv6 local connections: 90 | #host all all ::1/128 trust 91 | # Allow replication connections from localhost, by a user with the 92 | # replication privilege. 93 | #local replication postgres trust 94 | #host replication postgres 127.0.0.1/32 trust 95 | #host replication postgres ::1/128 trust 96 | -------------------------------------------------------------------------------- /scripts/docker/run-cluster-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start up a master and a single replica for testing 4 | CCP_IMAGE_TAG=centos7-9.6-1.2.7 5 | 6 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | 8 | echo "starting master container..." 9 | 10 | DATA_DIR=$DIR/.docker/master-data 11 | sudo rm -rf $DATA_DIR 12 | sudo mkdir -p $DATA_DIR 13 | sudo chmod 777 $DATA_DIR 14 | sudo chcon -Rt svirt_sandbox_file_t $DATA_DIR 15 | 16 | CONFIG_DIR=$DIR/.docker/master-config 17 | sudo rm -rf $CONFIG_DIR 18 | sudo mkdir -p $CONFIG_DIR 19 | sudo chmod 777 $CONFIG_DIR 20 | sudo chcon -Rt svirt_sandbox_file_t $CONFIG_DIR 21 | 22 | 23 | cp $DIR/master-config/postgresql.conf $CONFIG_DIR 24 | cp $DIR/master-config/pg_hba.conf $CONFIG_DIR 25 | sudo chmod 600 $CONFIG_DIR/pg_hba.conf $CONFIG_DIR/postgresql.conf 26 | sudo chown postgres:postgres $CONFIG_DIR/pg_hba.conf $CONFIG_DIR/postgresql.conf 27 | 28 | docker stop master 29 | docker rm master 30 | 31 | docker run \ 32 | -p 12000:5432 \ 33 | -v $CONFIG_DIR:/pgconf \ 34 | -v $DATA_DIR:/pgdata \ 35 | -e TEMP_BUFFERS=9MB \ 36 | -e MAX_CONNECTIONS=101 \ 37 | -e SHARED_BUFFERS=129MB \ 38 | -e MAX_WAL_SENDERS=7 \ 39 | -e WORK_MEM=5MB \ 40 | -e PG_MODE=master \ 41 | -e PG_MASTER_USER=masteruser \ 42 | -e PG_MASTER_PASSWORD=password \ 43 | -e PG_USER=testuser \ 44 | -e PG_ROOT_PASSWORD=password \ 45 | -e PG_PASSWORD=password \ 46 | -e PG_DATABASE=userdb \ 47 | -e PGHOST=/tmp \ 48 | --name=master \ 49 | --hostname=master \ 50 | -d crunchydata/crunchy-postgres:$CCP_IMAGE_TAG 51 | 52 | echo "sleeping a bit before starting replica..." 53 | sleep 10 54 | echo "starting replica container..." 55 | 56 | DATA_DIR=$DIR/.docker/replica-data 57 | sudo rm -rf $DATA_DIR 58 | sudo mkdir -p $DATA_DIR 59 | sudo chown postgres:postgres $DATA_DIR 60 | sudo chcon -Rt svirt_sandbox_file_t $DATA_DIR 61 | 62 | CONFIG_DIR=$DIR/.docker/replica-config 63 | sudo rm -rf $CONFIG_DIR 64 | sudo mkdir -p $CONFIG_DIR 65 | sudo chmod 777 $CONFIG_DIR 66 | sudo chcon -Rt svirt_sandbox_file_t $CONFIG_DIR 67 | 68 | cp $DIR/replica-config/postgresql.conf $CONFIG_DIR 69 | cp $DIR/replica-config/pg_hba.conf $CONFIG_DIR 70 | sudo chmod 600 $CONFIG_DIR/pg_hba.conf $CONFIG_DIR/postgresql.conf 71 | sudo chown postgres:postgres $CONFIG_DIR/pg_hba.conf $CONFIG_DIR/postgresql.conf 72 | 73 | sudo docker stop replica 74 | sudo docker rm replica 75 | 76 | sudo docker run \ 77 | -p 12002:5432 \ 78 | -v $CONFIG_DIR:/pgconf \ 79 | -v $DATA_DIR:/pgdata \ 80 | -e TEMP_BUFFERS=9MB \ 81 | -e MAX_CONNECTIONS=101 \ 82 | -e SHARED_BUFFERS=129MB \ 83 | -e MAX_WAL_SENDERS=7 \ 84 | -e WORK_MEM=5MB \ 85 | -e PG_MODE=slave \ 86 | -e PG_MASTER_USER=masteruser \ 87 | -e PG_MASTER_PASSWORD=password \ 88 | -e PG_MASTER_HOST=master \ 89 | --link master:master \ 90 | -e PG_MASTER_PORT=5432 \ 91 | -e PG_USER=testuser \ 92 | -e PG_ROOT_PASSWORD=password \ 93 | -e PG_PASSWORD=password \ 94 | -e PG_DATABASE=userdb \ 95 | -e PGHOST=/tmp \ 96 | --name=replica \ 97 | --hostname=replica \ 98 | -d crunchydata/crunchy-postgres:$CCP_IMAGE_TAG 99 | 100 | -------------------------------------------------------------------------------- /scripts/docker/run-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start up a master and a single replica for testing 4 | CCP_IMAGE_TAG=centos7-9.6-1.2.7 5 | 6 | echo "starting master container..." 7 | 8 | DATA_DIR=$HOME/master-data 9 | sudo rm -rf $DATA_DIR 10 | sudo mkdir -p $DATA_DIR 11 | sudo chmod 777 $DATA_DIR 12 | sudo chcon -Rt svirt_sandbox_file_t $DATA_DIR 13 | 14 | docker stop master 15 | docker rm master 16 | 17 | docker run \ 18 | -p 12000:5432 \ 19 | -v $DATA_DIR:/pgdata \ 20 | -e TEMP_BUFFERS=9MB \ 21 | -e MAX_CONNECTIONS=101 \ 22 | -e SHARED_BUFFERS=129MB \ 23 | -e MAX_WAL_SENDERS=7 \ 24 | -e WORK_MEM=5MB \ 25 | -e PG_MODE=master \ 26 | -e PG_MASTER_USER=masteruser \ 27 | -e PG_MASTER_PASSWORD=password \ 28 | -e PG_USER=testuser \ 29 | -e PG_ROOT_PASSWORD=password \ 30 | -e PG_PASSWORD=password \ 31 | -e PG_DATABASE=userdb \ 32 | -e PGHOST=/tmp \ 33 | --name=master \ 34 | --hostname=master \ 35 | -d crunchydata/crunchy-postgres:$CCP_IMAGE_TAG 36 | 37 | echo "sleeping a bit before starting replica..." 38 | sleep 10 39 | echo "starting replica container..." 40 | 41 | DATA_DIR=$HOME/replica-data 42 | sudo rm -rf $DATA_DIR 43 | sudo mkdir -p $DATA_DIR 44 | sudo chown postgres:postgres $DATA_DIR 45 | sudo chcon -Rt svirt_sandbox_file_t $DATA_DIR 46 | 47 | sudo docker stop replica 48 | sudo docker rm replica 49 | 50 | sudo docker run \ 51 | -p 12002:5432 \ 52 | -v $DATA_DIR:/pgdata \ 53 | -e TEMP_BUFFERS=9MB \ 54 | -e MAX_CONNECTIONS=101 \ 55 | -e SHARED_BUFFERS=129MB \ 56 | -e MAX_WAL_SENDERS=7 \ 57 | -e WORK_MEM=5MB \ 58 | -e PG_MODE=slave \ 59 | -e PG_MASTER_USER=masteruser \ 60 | -e PG_MASTER_PASSWORD=password \ 61 | -e PG_MASTER_HOST=master \ 62 | --link master:master \ 63 | -e PG_MASTER_PORT=5432 \ 64 | -e PG_USER=testuser \ 65 | -e PG_ROOT_PASSWORD=password \ 66 | -e PG_PASSWORD=password \ 67 | -e PG_DATABASE=userdb \ 68 | -e PGHOST=/tmp \ 69 | --name=replica \ 70 | --hostname=replica \ 71 | -d crunchydata/crunchy-postgres:$CCP_IMAGE_TAG 72 | 73 | -------------------------------------------------------------------------------- /scripts/docker/run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2016 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | PROXY_TAG=centos7-1.0.0-beta 17 | PROXY_PORT=5432 18 | PROXY_ADMIN_PORT=10000 19 | 20 | CONTAINER_NAME=crunchy-proxy 21 | 22 | CONFIG=$(readlink -f ./config.yaml) 23 | 24 | if [ -f /etc/redhat-release ]; then 25 | sudo chcon -Rt svirt_sandbox_file_t $(readlink -f $CONFIG) 26 | fi 27 | 28 | docker rm $CONTAINER_NAME 29 | docker run -d --name=$CONTAINER_NAME \ 30 | -p 127.0.0.1:$PROXY_PORT:$PROXY_PORT \ 31 | -p 127.0.0.1:$PROXY_ADMIN_PORT:$PROXY_ADMIN_PORT \ 32 | -v $CONFIG:/config/config.yaml \ 33 | crunchydata/crunchy-proxy:$PROXY_TAG 34 | -------------------------------------------------------------------------------- /scripts/docker/run-pgpool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2016 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | echo "starting pgpool container...." 17 | 18 | sudo docker stop pgpool 19 | sudo docker rm pgpool 20 | 21 | sudo docker run \ 22 | -p 12003:5432 \ 23 | --link master:master \ 24 | --link replica:replica \ 25 | -e PG_MASTER_SERVICE_NAME=master \ 26 | -e PG_SLAVE_SERVICE_NAME=replica \ 27 | -e PG_USERNAME=testuser \ 28 | -e PG_PASSWORD=password \ 29 | -e PG_DATABASE=postgres \ 30 | --name=pgpool \ 31 | --hostname=pgpool \ 32 | -d crunchydata/crunchy-pgpool:$CCP_IMAGE_TAG 33 | 34 | -------------------------------------------------------------------------------- /scripts/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # next set is only for setting up enterprise crunchy postgres repo 5 | # not required if you build on centos 6 | # 7 | #sudo mkdir /opt/crunchy 8 | #sudo cp $BUILDBASE/conf/crunchypg95.repo /etc/yum.repos.d 9 | #sudo cp $BUILDBASE/conf/CRUNCHY* /opt/crunchy 10 | #sudo yum -y install postgresql95-server 11 | 12 | sudo yum -y install net-tools bind-utils wget unzip git 13 | 14 | # 15 | # this set is required to build the docs 16 | # 17 | sudo yum -y install asciidoc ruby 18 | gem install --pre asciidoctor-pdf 19 | wget -O $HOME/bootstrap-4.5.0.zip http://laurent-laville.org/asciidoc/bootstrap/bootstrap-4.5.0.zip 20 | asciidoc --backend install $HOME/bootstrap-4.5.0.zip 21 | mkdir -p $HOME/.asciidoc/backends/bootstrap/js 22 | #cp $GOPATH/src/github.com/crunchydata/crunchy-containers/docs/bootstrap.js \ 23 | #$HOME/.asciidoc/backends/bootstrap/js/ 24 | unzip $HOME/bootstrap-4.5.0.zip $HOME/.asciidoc/backends/bootstrap/ 25 | 26 | -------------------------------------------------------------------------------- /scripts/vagrant/README: -------------------------------------------------------------------------------- 1 | # Vagrant Test/Development Environment 2 | 3 | This directory contains the configurations for creating a virtual development 4 | and test environment for this project. 5 | 6 | The Vagrantfile contains the following VM configurations: 7 | 8 | * master - the master node 9 | * replica - a replica node 10 | 11 | The setup and configuration of these nodes is provided by Ansible. Playbooks 12 | have been defined for each node as well as separate node specific variables. 13 | 14 | ## Usage 15 | 16 | Create and provision the 'master' and 'replica' nodes. 17 | 18 | ``` 19 | $> vagrant up 20 | ``` 21 | 22 | Initialize the replica. 23 | 24 | ``` 25 | $> vagrant ssh replica -- 'bash -s' < ./initialize-replica.sh 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /scripts/vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | Vagrant.configure("2") do |config| 4 | 5 | # vagrant vbguest plugin configurations 6 | # 7 | # Installation: 8 | # $> vagrant plugin install vagrant-vbguest 9 | # 10 | # https://github.com/dotless-de/vagrant-vbguest 11 | # 12 | # Virtual Box Guest Additions are not necessary for this setup. Therefore, 13 | # there is no need to install or update the latest version. The centos/7 14 | # base box does not have Guest Additions installed by default, therefore, 15 | # if it becomes necessary to utilize functionality provide by them, then 16 | # 'auto_update' should be set to 'true'. 17 | config.vbguest.auto_update = false 18 | 19 | # vagrant hostmanager plugin configurations 20 | # 21 | # Installation: 22 | # $> vagrant plugin install vagrant-hostmanager 23 | # 24 | # https://github.com/devopsgroup-io/vagrant-hostmanager 25 | # 26 | # Configure to only manage 'host' /etc/hosts file. Password may be required 27 | # pending the host's environment. 28 | config.hostmanager.enabled = true 29 | config.hostmanager.manage_host = true 30 | config.hostmanager.manage_guest = false 31 | config.hostmanager.ignore_private_ip = false 32 | 33 | # Vagrant binds the FQDN to localhost in /etc/hosts when a hostname if provided 34 | # as part of the configuration. There is no way to disable this behavior, 35 | # therefore we need to correct this by removing it. 36 | config.vm.provision "shell", 37 | inline: "sed -i'' \"/^127.0.0.1\\s$(hostname --fqdn)\\s$(hostname --short)$/d\" /etc/hosts" 38 | 39 | # The VM's DNS resolver should point to the host. Enabling the 40 | # 'natdnshostresolver' virualbox option on the VM will allow for this 41 | # behavior. 42 | config.vm.provider :virtualbox do |vb| 43 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 44 | end 45 | 46 | # ----------------- 47 | # VM CONFIGURATIONS 48 | # ----------------- 49 | 50 | # PostgreSQL Master VM Configuration 51 | config.vm.define "master" do |master| 52 | master.vm.box = "centos/7" 53 | 54 | master.vm.hostname = "master.crunchy.lab" 55 | 56 | master.vm.network "private_network", ip: "192.168.56.100" 57 | 58 | # Syncing the default folder is not required. There is no need to share 59 | # files between the Host and the Guest at this point. Therefore, to avoid 60 | # any potential permission or other issues, it is better to just disable 61 | # it. 62 | master.vm.synced_folder ".", "/vagrant", disabled: true 63 | 64 | # Provision the system via Ansible. 65 | master.vm.provision "ansible" do |ansible| 66 | ansible.playbook = "ansible/master.yml" 67 | ansible.verbose = "vv" 68 | ansible.sudo = true 69 | end 70 | end 71 | 72 | # PostgreSQL Replica VM Configuration 73 | config.vm.define "replica" do |replica| 74 | replica.vm.box = "centos/7" 75 | 76 | replica.vm.hostname = "replica.crunchy.lab" 77 | 78 | replica.vm.network "private_network", ip: "192.168.56.101" 79 | 80 | # Syncing the default folder is not required. There is no need to share 81 | # files between the Host and the Guest at this point. Therefore, to avoid 82 | # any potential permission or other issues, it is better to just disable 83 | # it. 84 | replica.vm.synced_folder ".", "/vagrant", disabled: true 85 | 86 | # Provision the system via Ansible. 87 | replica.vm.provision "ansible" do |ansible| 88 | ansible.playbook = "ansible/replica.yml" 89 | ansible.verbose = "vv" 90 | ansible.sudo = true 91 | end 92 | end 93 | 94 | # pgpool VM Configuration 95 | # 96 | # This VM is for performance testing only. 97 | config.vm.define "pgpool" do |pgp| 98 | pgp.vm.box = "centos/7" 99 | 100 | pgp.vm.hostname = "pgpool.crunchy.lab" 101 | 102 | pgp.vm.network "private_network", ip: "192.168.56.201" 103 | 104 | # Syncing the default folder is not required. There is no need to share 105 | # files between the Host and the Guest at this point. Therefore, to avoid 106 | # any potential permission or other issues, it is better to just disable 107 | # it. 108 | pgp.vm.synced_folder ".", "/vagrant", disabled: true 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/files/certs: -------------------------------------------------------------------------------- 1 | ../../../certs -------------------------------------------------------------------------------- /scripts/vagrant/ansible/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: restart postgresql 4 | service: 5 | name: postgresql-9.6 6 | state: restarted 7 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/master.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: all 4 | 5 | tasks: 6 | # Include common tasks. 7 | - include: "tasks/common-tasks.yml" 8 | 9 | # Include common PG variables and tasks. 10 | - include_vars: "vars/pg-common-vars.yml" 11 | - include: "tasks/pg-common-tasks.yml" 12 | 13 | # Include master specific configuration variables. 14 | - include_vars: "vars/master-vars.yml" 15 | 16 | # Configure. 17 | - include: "tasks/pg-configure-tasks.yml" 18 | 19 | # Install SSL Certificate 20 | - include: "tasks/pg-ssl-tasks.yml" 21 | 22 | - name: "Enable and Start PostgreSQL" 23 | service: 24 | name: "postgresql-9.6" 25 | enabled: yes 26 | state: restarted 27 | 28 | # Setup Replication User 29 | - name: "Create Replication User" 30 | command: "psql -U postgres -c \"CREATE USER {{postgresql_replication_user}} REPLICATION LOGIN CONNECTION LIMIT 5 ENCRYPTED PASSWORD '{{postgresql_replication_user_passwd}}';\"" 31 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/replica.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: all 4 | 5 | tasks: 6 | # Include common tasks. 7 | - include: "tasks/common-tasks.yml" 8 | 9 | # Include common PG variables and tasks. 10 | - include_vars: "vars/pg-common-vars.yml" 11 | - include: "tasks/pg-common-tasks.yml" 12 | 13 | # Include replica specific configuration variables. 14 | #- include_vars: "vars/replica-vars.yml" 15 | 16 | # Setup replica. 17 | # TODO 18 | 19 | # Configure. 20 | # - include: "tasks/pg-configure-tasks.yml" 21 | 22 | # Install SSL Certificates 23 | #- include: "tasks/pg-ssl-tasks.yml" 24 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/tasks/common-tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "Install ntp" 4 | package: 5 | name: "ntp" 6 | state: installed 7 | 8 | - name: "Update ntp" 9 | command: "ntpdate -b -u time.nist.gov" 10 | 11 | - name: "Enable ntp" 12 | service: 13 | name: "ntpd" 14 | enabled: yes 15 | state: started 16 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/tasks/pg-common-tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "Install PGDG Yum Repo" 4 | yum: 5 | name: "https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm" 6 | state: installed 7 | 8 | - name: "Install PostgreSQL" 9 | package: 10 | name: "{{ item }}" 11 | state: installed 12 | with_items: "{{ postgresql_packages | list }}" 13 | 14 | - name: "Ensure PostgreSQL data directory exists." 15 | file: 16 | path: "{{ postgresql_data_dir }}" 17 | owner: "{{ postgresql_user }}" 18 | group: "{{ postgresql_group }}" 19 | state: directory 20 | mode: 0700 21 | 22 | - name: "Check if database is initialized." 23 | stat: 24 | path: "{{ postgresql_data_dir }}/PG_VERSION" 25 | register: pgdata_dir_version 26 | 27 | - name: "Ensure PostgreSQL database is initialized." 28 | command: "{{ postgresql_bin_dir }}/initdb -D {{ postgresql_data_dir }}" 29 | when: not pgdata_dir_version.stat.exists 30 | become: yes 31 | become_user: "{{ postgresql_user }}" 32 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/tasks/pg-configure-tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "Configure postgresql.conf" 4 | lineinfile: 5 | dest: "{{ postgresql_config_file }}" 6 | regexp: "^#?{{ item.option }}.+$" 7 | line: "{{ item.option }} = '{{ item.value }}'" 8 | state: "{{ item.state | default('present') }}" 9 | with_items: "{{ postgresql_config_options }}" 10 | 11 | # Configure pg_hba.conf 12 | - name: "Configure pg_hba.conf" 13 | lineinfile: 14 | dest: "{{ postgresql_hba_file }}" 15 | regexp: "^#?{{ item.type }}.+S" 16 | line: "{{ item.type}}\t{{ item.database }}\t{{ item.user }}\t{{ item.address}}\t{{ item.method }}" 17 | state: "{{ item.state | default('present') }}" 18 | with_items: "{{ postgresql_hba_entries }}" 19 | 20 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/tasks/pg-intialize-tasks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "Check if database is initialized." 4 | stat: 5 | path: "{{ postgresql_data_dir }}/PG_VERSION" 6 | register: pgdata_dir_version 7 | 8 | - name: "Ensure PostgreSQL database is initialized." 9 | command: "{{ postgresql_bin_dir }}/initdb -D {{ postgresql_data_dir }}" 10 | when: not pgdata_dir_version.stat.exists 11 | become: yes 12 | become_user: "{{ postgresql_user }}" 13 | 14 | # TODO add enable and start service. 15 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/tasks/pg-ssl-tasks.yml: -------------------------------------------------------------------------------- 1 | - name: "Install SSL Server Certificate" 2 | copy: 3 | src: "files/certs/server/server.crt" 4 | dest: "{{ postgresql_data_dir }}/server.crt" 5 | owner: "{{ postgresql_user }}" 6 | group: "{{ postgresql_user }}" 7 | mode: "0600" 8 | 9 | - name: "Install SSL Server Key" 10 | copy: 11 | src: "files/certs/server/server.key" 12 | dest: "{{ postgresql_data_dir }}/server.key" 13 | owner: "{{ postgresql_user }}" 14 | group: "{{ postgresql_user }}" 15 | mode: "0600" 16 | 17 | - name: "Install Root CA Certificate" 18 | copy: 19 | src: "files/certs/server/ca.crt" 20 | dest: "{{ postgresql_data_dir }}/ca.crt" 21 | owner: "{{ postgresql_user }}" 22 | group: "{{ postgresql_user }}" 23 | mode: "0600" 24 | 25 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/vars/master-vars.yml: -------------------------------------------------------------------------------- 1 | postgresql_config_options: 2 | - option: "listen_addresses" 3 | value: "*" 4 | 5 | # SSL Options 6 | - option: "ssl" 7 | value: "on" 8 | - option: "ssl_cert_file" 9 | value: "server.crt" 10 | - option: "ssl_key_file" 11 | value: "server.key" 12 | - option: "ssl_ca_file" 13 | value: "ca.crt" 14 | 15 | # Replication Options 16 | - option: "wal_level" 17 | value: "hot_standby" 18 | - option: "max_wal_senders" 19 | value: "3" 20 | # Replica options - ignored by master 21 | - option: "hot_standby" 22 | value: on 23 | 24 | postgresql_hba_entries: 25 | # Non-SSL entries 26 | - type: host 27 | database: all 28 | user: postgres 29 | address: 192.168.56.0/24 30 | method: trust 31 | state: present 32 | 33 | - type: host 34 | database: replication 35 | user: rep 36 | address: "{{ replica_ip }}/32" 37 | method: trust 38 | state: present 39 | 40 | # SSL entries 41 | - type: hostssl 42 | database: all 43 | user: all 44 | address: 0.0.0.0/0 45 | method: md5 46 | state: present 47 | 48 | - type: hostssl 49 | database: replication 50 | user: rep 51 | address: "{{ replica_ip }}/32" 52 | method: cert 53 | state: present 54 | 55 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/vars/pg-common-vars.yml: -------------------------------------------------------------------------------- 1 | postgresql_version: "9.6" 2 | 3 | postgresql_user: "postgres" 4 | 5 | postgresql_group: "postgres" 6 | 7 | postgresql_replication_user: "rep" 8 | postgresql_replication_user_passwd: "password" 9 | 10 | postgresql_data_dir: "/var/lib/pgsql/{{ postgresql_version}}/data" 11 | 12 | postgresql_bin_dir: "/usr/pgsql-{{ postgresql_version }}/bin" 13 | 14 | postgresql_config_file: "{{ postgresql_data_dir }}/postgresql.conf" 15 | 16 | postgresql_hba_file: "{{ postgresql_data_dir }}/pg_hba.conf" 17 | 18 | postgresql_config_options: [] 19 | 20 | postgresql_hba_entries: [] 21 | 22 | postgresql_packages: 23 | - postgresql96 24 | - postgresql96-libs 25 | - postgresql96-server 26 | - postgresql96-devel 27 | 28 | replica_ip: "192.168.56.101" 29 | -------------------------------------------------------------------------------- /scripts/vagrant/ansible/vars/replica-vars.yml: -------------------------------------------------------------------------------- 1 | postgresql_config_options: 2 | - option: "listen_addresses" 3 | value: "*" 4 | 5 | # SSL Options 6 | - option: "ssl" 7 | value: "on" 8 | - option: "ssl_cert_file" 9 | value: "server.crt" 10 | - option: "ssl_key_file" 11 | value: "server.key" 12 | - option: "ssl_ca_file" 13 | value: "ca.crt" 14 | 15 | postgresql_hba_entries: 16 | - type: hostssl 17 | database: all 18 | user: all 19 | address: 0.0.0.0/0 20 | method: md5 21 | state: present 22 | 23 | - type: hostssl 24 | database: replication 25 | user: masteruser 26 | address: 0.0.0.0/0 27 | method: cert 28 | state: present 29 | -------------------------------------------------------------------------------- /scripts/vagrant/initialize-replica.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PG_BASEBACKUP=/usr/bin/pg_basebackup 4 | PG_USER=rep 5 | PG_MASTER_IP=192.168.56.100 6 | PG_DATA_DIR=/var/lib/pgsql/9.6/data 7 | 8 | SUDO='sudo -i PWD=/var/lib/pgsql -u postgres' 9 | 10 | echo "Removing Data Directory..." 11 | $SUDO rm -rf /var/lib/pgsql/9.6/data 12 | echo "Done." 13 | 14 | 15 | # Initialize the replica data directory based on the master. 16 | # 17 | # pg_basebackup parameters (provided for reference): 18 | # 19 | # * -P - Show progress information 20 | # * -R - Write recovery.conf file 21 | # * -c fast - set fast checkpointing 22 | # * -X stream - set xlog method to streaming 23 | # * -h - the ip of the master 24 | # * -U - the replication user 25 | # * -D - the directory to receive the base backup. 26 | # 27 | echo "Performing Base Backup..." 28 | $SUDO $PG_BASEBACKUP -P -R -c fast -X stream -h $PG_MASTER_IP -U $PG_USER -D $PG_DATA_DIR 29 | 30 | echo "Setting up recovery.conf..." 31 | $SUDO "cat > /var/lib/pgsql/9.6/data/recovery.conf << 'EOF' 32 | trigger_file = '$PG_DATA_DIR/postgresql.trigger' 33 | EOF 34 | " 35 | echo "Done." 36 | 37 | 38 | -------------------------------------------------------------------------------- /server/admin.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package server 16 | 17 | import ( 18 | "database/sql" 19 | "fmt" 20 | "net" 21 | "time" 22 | 23 | _ "github.com/lib/pq" // required 24 | "golang.org/x/net/context" 25 | "google.golang.org/grpc" 26 | 27 | "github.com/crunchydata/crunchy-proxy/common" 28 | "github.com/crunchydata/crunchy-proxy/config" 29 | pb "github.com/crunchydata/crunchy-proxy/server/serverpb" 30 | "github.com/crunchydata/crunchy-proxy/util/grpcutil" 31 | "github.com/crunchydata/crunchy-proxy/util/log" 32 | ) 33 | 34 | type AdminServer struct { 35 | grpc *grpc.Server 36 | server *Server 37 | nodeHealth map[string]bool 38 | } 39 | 40 | func NewAdminServer(s *Server) *AdminServer { 41 | admin := &AdminServer{ 42 | server: s, 43 | nodeHealth: make(map[string]bool, 0), 44 | } 45 | 46 | admin.grpc = grpc.NewServer() 47 | 48 | pb.RegisterAdminServer(admin.grpc, admin) 49 | 50 | return admin 51 | } 52 | 53 | func (s *AdminServer) Nodes(ctx context.Context, req *pb.NodeRequest) (*pb.NodeResponse, error) { 54 | var response pb.NodeResponse 55 | 56 | response.Nodes = make(map[string]string, 0) 57 | 58 | for name, node := range config.GetNodes() { 59 | response.Nodes[name] = node.HostPort 60 | } 61 | 62 | return &response, nil 63 | } 64 | 65 | func (s *AdminServer) Pools(ctx context.Context, req *pb.PoolRequest) (*pb.PoolResponse, error) { 66 | var response pb.PoolResponse 67 | 68 | response.Pools = append(response.Pools, "Pool") 69 | 70 | return &response, nil 71 | } 72 | 73 | func (s *AdminServer) Shutdown(req *pb.ShutdownRequest, stream pb.Admin_ShutdownServer) error { 74 | // Stop the Proxy Server 75 | s.server.proxy.Stop() 76 | 77 | // Stop the Admin grpc Server 78 | s.grpc.Stop() 79 | 80 | return nil 81 | } 82 | 83 | func (s *AdminServer) Health(ctx context.Context, req *pb.HealthRequest) (*pb.HealthResponse, error) { 84 | var response pb.HealthResponse 85 | 86 | response.Health = s.nodeHealth 87 | 88 | return &response, nil 89 | } 90 | 91 | func (s *AdminServer) Statistics(context.Context, *pb.StatisticsRequest) (*pb.StatisticsResponse, error) { 92 | var response pb.StatisticsResponse 93 | 94 | response.Queries = s.server.proxy.Stats() 95 | 96 | return &response, nil 97 | } 98 | 99 | func (s *AdminServer) Version(context.Context, *pb.VersionRequest) (*pb.VersionResponse, error) { 100 | var response pb.VersionResponse 101 | 102 | response.Version = "1.0.0beta" 103 | 104 | return &response, nil 105 | } 106 | 107 | func (s *AdminServer) Serve(l net.Listener) { 108 | log.Infof("Admin Server listening on: %s", l.Addr()) 109 | defer s.server.waitGroup.Done() 110 | 111 | go s.startHealthCheck() 112 | 113 | err := s.grpc.Serve(l) 114 | l.Close() 115 | 116 | if !grpcutil.IsClosedConnection(err) { 117 | log.Infof("Server Error: %s", err) 118 | } 119 | } 120 | 121 | func (s *AdminServer) startHealthCheck() { 122 | nodes := config.GetNodes() 123 | hcConfig := config.GetHealthCheckConfig() 124 | 125 | for { 126 | for name, node := range nodes { 127 | /* Connect to node */ 128 | conn, err := getDBConnection(node) 129 | 130 | if err != nil { 131 | log.Errorf("healthcheck: error creating connection to '%s'", name) 132 | log.Errorf("healthcheck: %s", err.Error()) 133 | } 134 | 135 | /* Perform Health Check Query */ 136 | rows, err := conn.Query(hcConfig.Query) 137 | 138 | if err != nil { 139 | log.Errorf("healthcheck: query failed: %s", err.Error()) 140 | s.nodeHealth[name] = false 141 | continue 142 | } 143 | 144 | rows.Close() 145 | 146 | /* Update health status */ 147 | s.nodeHealth[name] = true 148 | 149 | conn.Close() 150 | } 151 | 152 | time.Sleep(time.Duration(hcConfig.Delay) * time.Second) 153 | } 154 | } 155 | 156 | func getDBConnection(node common.Node) (*sql.DB, error) { 157 | host, port, _ := net.SplitHostPort(node.HostPort) 158 | creds := config.GetCredentials() 159 | 160 | connectionString := fmt.Sprintf("host=%s port=%s ", host, port) 161 | connectionString += fmt.Sprintf(" user=%s", creds.Username) 162 | connectionString += fmt.Sprintf(" database=%s", creds.Database) 163 | 164 | connectionString += fmt.Sprintf(" sslmode=%s", creds.SSL.SSLMode) 165 | connectionString += " application_name=proxy_healthcheck" 166 | 167 | if creds.Password != "" { 168 | connectionString += fmt.Sprintf(" password=%s", creds.Password) 169 | } 170 | 171 | if creds.SSL.Enable { 172 | connectionString += fmt.Sprintf(" sslcert=%s", creds.SSL.SSLCert) 173 | connectionString += fmt.Sprintf(" sslkey=%s", creds.SSL.SSLKey) 174 | connectionString += fmt.Sprintf(" sslrootcert=%s", creds.SSL.SSLRootCA) 175 | } 176 | 177 | /* Build connection string. */ 178 | for key, value := range creds.Options { 179 | connectionString += fmt.Sprintf(" %s=%s", key, value) 180 | } 181 | 182 | log.Debugf("healthcheck: Opening connection with parameters: %s", 183 | connectionString) 184 | 185 | dbConn, err := sql.Open("postgres", connectionString) 186 | 187 | if err != nil { 188 | log.Errorf("healthcheck: Error creating connection : %s", err.Error()) 189 | } 190 | 191 | return dbConn, err 192 | } 193 | -------------------------------------------------------------------------------- /server/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package server 16 | 17 | import ( 18 | "net" 19 | 20 | "github.com/crunchydata/crunchy-proxy/proxy" 21 | "github.com/crunchydata/crunchy-proxy/util/log" 22 | ) 23 | 24 | type ProxyServer struct { 25 | ch chan bool 26 | server *Server 27 | p *proxy.Proxy 28 | listener net.Listener 29 | } 30 | 31 | func NewProxyServer(s *Server) *ProxyServer { 32 | proxy := &ProxyServer{} 33 | proxy.ch = make(chan bool) 34 | proxy.server = s 35 | 36 | return proxy 37 | } 38 | 39 | func (s *ProxyServer) Serve(l net.Listener) error { 40 | log.Infof("Proxy Server listening on: %s", l.Addr()) 41 | defer s.server.waitGroup.Done() 42 | s.listener = l 43 | 44 | s.p = proxy.NewProxy() 45 | 46 | for { 47 | 48 | select { 49 | case <-s.ch: 50 | return nil 51 | default: 52 | } 53 | 54 | conn, err := l.Accept() 55 | 56 | if err != nil { 57 | continue 58 | } 59 | 60 | go s.p.HandleConnection(conn) 61 | } 62 | } 63 | 64 | func (s *ProxyServer) Stats() map[string]int32 { 65 | return s.p.Stats 66 | } 67 | 68 | func (s *ProxyServer) Stop() { 69 | s.listener.Close() 70 | close(s.ch) 71 | } 72 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package server 16 | 17 | import ( 18 | "net" 19 | "sync" 20 | 21 | "github.com/crunchydata/crunchy-proxy/config" 22 | "github.com/crunchydata/crunchy-proxy/util/log" 23 | ) 24 | 25 | type Server struct { 26 | admin *AdminServer 27 | proxy *ProxyServer 28 | waitGroup *sync.WaitGroup 29 | } 30 | 31 | func NewServer() *Server { 32 | s := &Server{ 33 | waitGroup: &sync.WaitGroup{}, 34 | } 35 | 36 | s.admin = NewAdminServer(s) 37 | 38 | s.proxy = NewProxyServer(s) 39 | 40 | return s 41 | } 42 | 43 | func (s *Server) Start() { 44 | proxyConfig := config.GetProxyConfig() 45 | adminConfig := config.GetAdminConfig() 46 | 47 | log.Info("Admin Server Starting...") 48 | adminListener, err := net.Listen("tcp", adminConfig.HostPort) 49 | 50 | if err != nil { 51 | log.Fatal(err.Error()) 52 | return 53 | } 54 | 55 | s.waitGroup.Add(1) 56 | go s.admin.Serve(adminListener) 57 | 58 | log.Info("Proxy Server Starting...") 59 | proxyListener, err := net.Listen("tcp", proxyConfig.HostPort) 60 | if err != nil { 61 | log.Fatal(err.Error()) 62 | return 63 | } 64 | 65 | s.waitGroup.Add(1) 66 | go s.proxy.Serve(proxyListener) 67 | 68 | s.waitGroup.Wait() 69 | 70 | log.Info("Server Exiting...") 71 | } 72 | -------------------------------------------------------------------------------- /server/serverpb/admin.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | syntax="proto3"; 16 | 17 | package crunchyproxy.server.serverpb; 18 | option go_package = "serverpb"; 19 | 20 | import "google/api/annotations.proto"; 21 | 22 | // NodeRequest requests a list of nodes. 23 | message NodeRequest { 24 | } 25 | 26 | // NodeResponse contains a list of nodes. 27 | message NodeResponse { 28 | map nodes = 1; 29 | } 30 | 31 | // PoolRequest requests a list of pools. 32 | message PoolRequest { 33 | } 34 | 35 | // PoolResponse contains a list of pools. 36 | message PoolResponse { 37 | repeated string pools = 1; 38 | } 39 | 40 | message HealthRequest { 41 | 42 | } 43 | 44 | message HealthResponse { 45 | map health = 1; 46 | } 47 | 48 | message StatisticsRequest { 49 | } 50 | 51 | message StatisticsResponse { 52 | map queries = 1; 53 | } 54 | 55 | // ShutdownRequest requests the server to shutdown. 56 | message ShutdownRequest { 57 | } 58 | 59 | // ShutdownResponse contains the the state of the proxy. 60 | message ShutdownResponse { 61 | bool success = 1; 62 | } 63 | 64 | message VersionRequest { 65 | } 66 | 67 | message VersionResponse { 68 | string version = 1; 69 | } 70 | 71 | service Admin { 72 | rpc Nodes(NodeRequest) returns (NodeResponse) { 73 | option (google.api.http) = { 74 | get: "/_admin/nodes" 75 | }; 76 | } 77 | 78 | rpc Pools(PoolRequest) returns (PoolResponse) { 79 | option (google.api.http) = { 80 | get: "/_admin/pools" 81 | }; 82 | } 83 | 84 | rpc Health(HealthRequest) returns (HealthResponse) { 85 | option (google.api.http) = { 86 | get: "/_admin/health" 87 | }; 88 | } 89 | 90 | rpc Statistics(StatisticsRequest) returns (StatisticsResponse) { 91 | option (google.api.http) = { 92 | get: "/_admin/stats" 93 | }; 94 | } 95 | 96 | rpc Shutdown(ShutdownRequest) returns (stream ShutdownResponse) { 97 | option (google.api.http) = { 98 | post: "/_admin/shutdown" 99 | body: "*" 100 | }; 101 | } 102 | 103 | rpc Version(VersionRequest) returns (VersionResponse) { 104 | option (google.api.http) = { 105 | get: "/_admin/version" 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /testclient/run-test-pgpool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go run testclient.go \ 3 | -rows=onerow \ 4 | -count=100 \ 5 | -hostport=localhost:12003 \ 6 | -userid=testuser \ 7 | -password=password \ 8 | -database=userdb 9 | -------------------------------------------------------------------------------- /testclient/run-test-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go run testclient.go \ 3 | -count=100 \ 4 | -rows=onerow \ 5 | -hostport=localhost:5432 \ 6 | -userid=postgres \ 7 | -password=password \ 8 | -database=postgres 9 | -------------------------------------------------------------------------------- /testclient/testclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | package main 16 | 17 | import ( 18 | "database/sql" 19 | "flag" 20 | _ "github.com/lib/pq" 21 | "log" 22 | "os" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | func main() { 28 | log.SetFlags(log.Ltime | log.Lmicroseconds) 29 | log.Println("main starting...") 30 | 31 | var rows, hostport, userid, password, database string 32 | var count int 33 | flag.StringVar(&rows, "rows", "onerow", "onerow or tworows") 34 | flag.StringVar(&hostport, "hostport", "localhost:5432", "host:port") 35 | flag.StringVar(&userid, "userid", "postgres", "postgres userid") 36 | flag.StringVar(&password, "password", "password", "postgres password") 37 | flag.StringVar(&database, "database", "postgres", "database") 38 | flag.IntVar(&count, "count", 1, "number of executions") 39 | flag.Parse() 40 | 41 | var conn *sql.DB 42 | var err error 43 | //os.Setenv("PGCONNECTION_TIMEOUT", "20") 44 | var hostportarr = strings.Split(hostport, ":") 45 | var dbHost = hostportarr[0] 46 | var dbPort = hostportarr[1] 47 | 48 | log.Println("connecting to host:" + dbHost + 49 | " port:" + dbPort + 50 | " user:" + userid + 51 | " password:" + password + 52 | " database:" + database) 53 | 54 | conn, err = GetDBConnection(dbHost, userid, dbPort, database, password) 55 | 56 | checkError(err) 57 | log.Println("got a connection") 58 | if conn != nil { 59 | log.Println("conn is not nil") 60 | } 61 | 62 | log.Println("execution starts") 63 | var startTime = time.Now() 64 | 65 | switch rows { 66 | case "onerow": 67 | for i := 0; i < count; i++ { 68 | OneRow(conn) 69 | } 70 | break 71 | case "tworows": 72 | TwoRows(conn) 73 | break 74 | } 75 | 76 | log.Println("execution ends") 77 | 78 | var endTime = time.Since(startTime) 79 | 80 | log.Printf("Duration %s\n", endTime) 81 | 82 | conn.Close() 83 | os.Exit(0) 84 | } 85 | 86 | func OneRow(conn *sql.DB) { 87 | var timestamp string 88 | err := conn.QueryRow("/* read */ select text(now())").Scan(×tamp) 89 | switch { 90 | case err == sql.ErrNoRows: 91 | log.Println("no rows returned") 92 | case err != nil: 93 | log.Println(err.Error()) 94 | default: 95 | log.Println(timestamp + " was returned") 96 | } 97 | } 98 | 99 | func TwoRows(conn *sql.DB) { 100 | var timestamp string 101 | rows, err := conn.Query("/* read */ select text(generate_series(1,2))") 102 | defer rows.Close() 103 | 104 | for rows.Next() { 105 | if err := rows.Scan(×tamp); err != nil { 106 | log.Println(err.Error()) 107 | } 108 | log.Println(timestamp) 109 | } 110 | if err = rows.Err(); err != nil { 111 | log.Println(err.Error()) 112 | } 113 | } 114 | 115 | func checkError(err error) { 116 | if err != nil { 117 | log.Println("Fatal error:" + err.Error()) 118 | os.Exit(1) 119 | } 120 | } 121 | 122 | func GetDBConnection(dbHost string, userid string, dbPort string, database string, password string) (*sql.DB, error) { 123 | 124 | var dbConn *sql.DB 125 | var err error 126 | 127 | if password == "" { 128 | dbConn, err = sql.Open("postgres", 129 | "sslmode=disable user="+userid+ 130 | " host="+dbHost+ 131 | " port="+dbPort+ 132 | " dbname="+database+ 133 | " sslmode=disable") 134 | } else { 135 | dbConn, err = sql.Open("postgres", 136 | "sslmode=disable user="+userid+ 137 | " host="+dbHost+ 138 | " port="+dbPort+ 139 | " dbname="+database+ 140 | " password="+password+ 141 | " sslmode=disable") 142 | } 143 | if err != nil { 144 | log.Println("error in getting connection :" + err.Error()) 145 | } 146 | return dbConn, err 147 | } 148 | -------------------------------------------------------------------------------- /tests/admin_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestAPIStream(t *testing.T) { 24 | log.SetFlags(log.Ltime | log.Lmicroseconds) 25 | log.Println("TestAPIStream was called") 26 | var startTime = time.Now() 27 | 28 | req, err := http.NewRequest("GET", "http://localhost:10000/api/stream", nil) 29 | if err != nil { 30 | log.Fatal("NewRequest: ", err) 31 | t.FailNow() 32 | } 33 | client := &http.Client{} 34 | 35 | resp, err := client.Do(req) 36 | if err != nil { 37 | log.Fatal("Do: ", err) 38 | t.FailNow() 39 | } 40 | 41 | defer resp.Body.Close() 42 | 43 | var endTime = time.Since(startTime) 44 | log.Printf("Duration %s\n", endTime) 45 | 46 | } 47 | 48 | func TestAPIConfig(t *testing.T) { 49 | log.SetFlags(log.Ltime | log.Lmicroseconds) 50 | log.Println("TestAPIConfig was called") 51 | var startTime = time.Now() 52 | 53 | req, err := http.NewRequest("GET", "http://localhost:10000/api/config", nil) 54 | if err != nil { 55 | log.Fatal("NewRequest: ", err) 56 | t.FailNow() 57 | } 58 | client := &http.Client{} 59 | 60 | resp, err := client.Do(req) 61 | if err != nil { 62 | log.Fatal("Do: ", err) 63 | t.FailNow() 64 | } 65 | 66 | defer resp.Body.Close() 67 | 68 | var endTime = time.Since(startTime) 69 | log.Printf("Duration %s\n", endTime) 70 | 71 | } 72 | func TestAPIStats(t *testing.T) { 73 | log.SetFlags(log.Ltime | log.Lmicroseconds) 74 | log.Println("TestAPIStats was called") 75 | var startTime = time.Now() 76 | 77 | req, err := http.NewRequest("GET", "http://localhost:10000/api/stats", nil) 78 | if err != nil { 79 | log.Fatal("NewRequest: ", err) 80 | t.FailNow() 81 | } 82 | client := &http.Client{} 83 | 84 | resp, err := client.Do(req) 85 | if err != nil { 86 | log.Fatal("Do: ", err) 87 | t.FailNow() 88 | } 89 | 90 | defer resp.Body.Close() 91 | 92 | var endTime = time.Since(startTime) 93 | log.Printf("Duration %s\n", endTime) 94 | 95 | } 96 | -------------------------------------------------------------------------------- /tests/audit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "bytes" 18 | "database/sql" 19 | "io/ioutil" 20 | "log" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestAudit(t *testing.T) { 26 | log.SetFlags(log.Ltime | log.Lmicroseconds) 27 | log.Println("TestAudit was called") 28 | var startTime = time.Now() 29 | conn, err := Connect() 30 | defer conn.Close() 31 | if err != nil { 32 | t.FailNow() 33 | } 34 | 35 | var timestamp string 36 | err = conn.QueryRow("/* read */ select text(now())").Scan(×tamp) 37 | switch { 38 | case err == sql.ErrNoRows: 39 | log.Println("no rows returned") 40 | t.FailNow() 41 | case err != nil: 42 | log.Println(err.Error()) 43 | t.FailNow() 44 | default: 45 | log.Println(timestamp + " was returned") 46 | } 47 | 48 | dat, err := ioutil.ReadFile("/tmp/audit.log") 49 | if err != nil { 50 | log.Println(err.Error()) 51 | t.FailNow() 52 | } 53 | 54 | if bytes.Contains(dat, []byte("msg")) { 55 | log.Println("audit records were found") 56 | } else { 57 | log.Println("audit records were not found") 58 | } 59 | 60 | var endTime = time.Since(startTime) 61 | log.Printf("Duration %s\n", endTime) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "database/sql" 18 | _ "github.com/lib/pq" 19 | "log" 20 | "strings" 21 | ) 22 | 23 | func Connect() (*sql.DB, error) { 24 | var conn *sql.DB 25 | log.SetFlags(log.Ltime | log.Lmicroseconds) 26 | 27 | var err error 28 | //os.Setenv("PGCONNECTION_TIMEOUT", "20") 29 | var hostportarr = strings.Split(HostPort, ":") 30 | var dbHost = hostportarr[0] 31 | var dbPort = hostportarr[1] 32 | 33 | log.Println("connecting to host:" + dbHost + " port:" + dbPort + " user:" + userid + " password:" + password + " database:" + database) 34 | conn, err = GetDBConnection(dbHost, userid, dbPort, database, password) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return conn, err 40 | } 41 | 42 | func GetDBConnection(dbHost string, userid string, dbPort string, database string, password string) (*sql.DB, error) { 43 | 44 | var dbConn *sql.DB 45 | var err error 46 | 47 | if password == "" { 48 | //log.Println("a open db with dbHost=[" + dbHost + "] userid=[" + userid + "] dbPort=[" + dbPort + "] database=[" + database + "]") 49 | dbConn, err = sql.Open("postgres", "sslmode=disable user="+userid+" host="+dbHost+" port="+dbPort+" dbname="+database) 50 | } else { 51 | //log.Println("b open db with dbHost=[" + dbHost + "] userid=[" + userid + "] dbPort=[" + dbPort + "] database=[" + database + "] password=[" + password + "]") 52 | dbConn, err = sql.Open("postgres", "sslmode=disable user="+userid+" host="+dbHost+" port="+dbPort+" dbname="+database+" password="+password) 53 | } 54 | if err != nil { 55 | log.Println("error in getting connection :" + err.Error()) 56 | } 57 | return dbConn, err 58 | } 59 | -------------------------------------------------------------------------------- /tests/healthcheck_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "bytes" 18 | "log" 19 | "net/http" 20 | "os/exec" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestHealthcheck(t *testing.T) { 26 | const SLEEP_TIME = 6 27 | log.SetFlags(log.Ltime | log.Lmicroseconds) 28 | log.Println("TestHealthcheck was called") 29 | var startTime = time.Now() 30 | conn, err := Connect() 31 | defer conn.Close() 32 | if err != nil { 33 | t.FailNow() 34 | } 35 | 36 | //read the proxy event stream which equates to a healthcheck 37 | //has just been performed...after which we will have a retry test 38 | //window 39 | _, err = http.Get("http://localhost:10000/api/stream") 40 | if err != nil { 41 | t.FailNow() 42 | } 43 | log.Println("after the GET") 44 | 45 | //shut down the replica 46 | var cmdStdout, cmdStderr bytes.Buffer 47 | var cmd *exec.Cmd 48 | cmd = exec.Command("docker", "stop", "replica") 49 | cmd.Stdout = &cmdStdout 50 | cmd.Stderr = &cmdStderr 51 | err = cmd.Run() 52 | if err != nil { 53 | log.Println("docker stop stdout=" + cmdStdout.String()) 54 | log.Println("docker stop stderr=" + cmdStderr.String()) 55 | t.FailNow() 56 | } 57 | log.Println("docker stop stdout=" + cmdStdout.String()) 58 | 59 | //sleep a bit to give the replica time to stop 60 | log.Println("sleeping to let replica shutdown") 61 | time.Sleep(time.Duration(SLEEP_TIME) * time.Second) 62 | 63 | //read the proxy event stream which should test that healthcheck 64 | //still works after a backend has been shutdown 65 | _, err = http.Get("http://localhost:10000/api/stream") 66 | if err != nil { 67 | t.FailNow() 68 | } 69 | log.Println("after the 2nd GET") 70 | 71 | //restart the replica after the test 72 | cmd = exec.Command("docker", "start", "replica") 73 | err = cmd.Run() 74 | if err != nil { 75 | log.Println("docker start stdout=" + cmdStdout.String()) 76 | log.Println("docker start stderr=" + cmdStderr.String()) 77 | t.FailNow() 78 | } 79 | log.Println("docker start stdout=" + cmdStdout.String()) 80 | //sleep a bit to give the replica time to restart 81 | log.Println("sleeping to let replica restart") 82 | time.Sleep(time.Duration(SLEEP_TIME) * time.Second) 83 | 84 | log.Printf("Duration %s\n", time.Since(startTime)) 85 | 86 | } 87 | -------------------------------------------------------------------------------- /tests/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "flag" 18 | "os" 19 | "testing" 20 | ) 21 | 22 | var HostPort string 23 | var rows, userid, password, database string 24 | 25 | func TestMain(m *testing.M) { 26 | flag.StringVar(&rows, "rows", "onerow", "onerow or tworows") 27 | flag.StringVar(&HostPort, "hostport", "localhost:5432", "host:port") 28 | flag.StringVar(&userid, "userid", "postgres", "postgres userid") 29 | flag.StringVar(&password, "password", "password", "postgres password") 30 | flag.StringVar(&database, "database", "postgres", "database") 31 | flag.Parse() 32 | os.Exit(m.Run()) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tests/master-only/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "masteronly", 3 | "HostPort": "localhost:5432", 4 | "AdminHostPort": "localhost:10000", 5 | "healthcheck": { 6 | "delay": 60, 7 | "query": "select now()" 8 | }, 9 | "pool": { 10 | "enabled": true, 11 | "capacity": 2 12 | }, 13 | "credentials": { 14 | "username": "postgres", 15 | "password": "password", 16 | "database": "postgres" 17 | }, 18 | "Adapters": [ 19 | "logging" 20 | ], 21 | "Master": { 22 | "HostPort": "127.0.0.1:12000", 23 | "metadata": { 24 | "cpu": "large", 25 | "disk": "fast" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/master-replica/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "sampleconfig", 3 | "HostPort": "localhost:5432", 4 | "AdminHostPort": "localhost:10000", 5 | "healthcheck": { 6 | "delay": 60, 7 | "query": "select now()" 8 | }, 9 | "pool": { 10 | "enabled": true, 11 | "capacity": 2 12 | }, 13 | "credentials": { 14 | "username": "postgres", 15 | "password": "password", 16 | "database": "postgres" 17 | }, 18 | "Adapters": [ 19 | "logging" 20 | ], 21 | "Master": { 22 | "HostPort": "127.0.0.1:12000", 23 | "metadata": { 24 | "cpu": "large", 25 | "disk": "fast" 26 | } 27 | }, 28 | "Replicas": [{ 29 | "HostPort": "127.0.0.1:12002", 30 | "metadata": { 31 | "cpu": "small", 32 | "disk": "slow" 33 | } 34 | }] 35 | } 36 | -------------------------------------------------------------------------------- /tests/overhead_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "database/sql" 18 | "log" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestOverhead(t *testing.T) { 24 | var proxyconn, conn *sql.DB 25 | var err error 26 | 27 | log.SetFlags(log.Ltime | log.Lmicroseconds) 28 | log.Println("TestOverhead was called") 29 | proxyconn, err = Connect() 30 | if err != nil { 31 | t.FailNow() 32 | } 33 | 34 | HostPort = "localhost:12000" 35 | conn, err = Connect() 36 | if err != nil { 37 | t.FailNow() 38 | } 39 | 40 | log.Println("") 41 | log.Println("Overhead (no annotation)") 42 | log.Println("") 43 | 44 | var timestamp string 45 | var proxyStartTime = time.Now() 46 | err = proxyconn.QueryRow("select now()").Scan(×tamp) 47 | switch { 48 | case err == sql.ErrNoRows: 49 | log.Println("no rows returned") 50 | t.FailNow() 51 | case err != nil: 52 | log.Println(err.Error()) 53 | t.FailNow() 54 | default: 55 | //log.Println(timestamp + " was returned") 56 | } 57 | 58 | proxyDuration := time.Since(proxyStartTime) 59 | log.Printf("Proxy Duration (no annotation) %s\n", proxyDuration) 60 | 61 | noProxyStartTime := time.Now() 62 | err = conn.QueryRow("select now()").Scan(×tamp) 63 | switch { 64 | case err == sql.ErrNoRows: 65 | log.Println("no rows returned") 66 | t.FailNow() 67 | case err != nil: 68 | log.Println(err.Error()) 69 | t.FailNow() 70 | default: 71 | //log.Println(timestamp + " was returned") 72 | } 73 | 74 | noProxyDuration := time.Since(noProxyStartTime) 75 | log.Printf("No Proxy Duration (no annotation) %s\n", noProxyDuration) 76 | 77 | log.Printf("Proxy Overhead (no annotation) %s\n", proxyDuration-noProxyDuration) 78 | proxyStartTime = time.Now() 79 | err = proxyconn.QueryRow("/* read */select now()").Scan(×tamp) 80 | switch { 81 | case err == sql.ErrNoRows: 82 | log.Println("no rows returned") 83 | t.FailNow() 84 | case err != nil: 85 | log.Println(err.Error()) 86 | t.FailNow() 87 | default: 88 | //log.Println(timestamp + " was returned") 89 | } 90 | 91 | log.Println("") 92 | log.Println("Overhead (annotation is supplied)") 93 | log.Println("") 94 | 95 | proxyDuration = time.Since(proxyStartTime) 96 | log.Printf("Proxy Duration (annotation) %s\n", proxyDuration) 97 | 98 | noProxyStartTime = time.Now() 99 | err = conn.QueryRow("/* read */select now()").Scan(×tamp) 100 | switch { 101 | case err == sql.ErrNoRows: 102 | log.Println("no rows returned") 103 | t.FailNow() 104 | case err != nil: 105 | log.Println(err.Error()) 106 | t.FailNow() 107 | default: 108 | //log.Println(timestamp + " was returned") 109 | } 110 | 111 | noProxyDuration = time.Since(noProxyStartTime) 112 | log.Printf("No Proxy Duration (annotation) %s\n", noProxyDuration) 113 | log.Printf("Proxy Overhead (annotation) %s\n", proxyDuration-noProxyDuration) 114 | 115 | proxyconn.Close() 116 | conn.Close() 117 | 118 | } 119 | -------------------------------------------------------------------------------- /tests/pgbench/README.md: -------------------------------------------------------------------------------- 1 | # Testing with pgbench 2 | 3 | Pgbench can be used to run general benchmark tests against the proxy. 4 | 5 | Currently, the following tests are provided as part of this test suite: 6 | 7 | * **simple-load-tests.sql** - These tests are meant to simply perform 8 | repetitive read only operations against the proxy. This test has two simple 9 | queries one that is annotated with the 'read' annotation and one that is not. 10 | The purpose of this test is to send the same number of queries to both backend 11 | nodes per pgbench transaction. 12 | 13 | * **concurrency-tests.sql** - These tests perform a series of simple insert, 14 | update and read operations against the proxy. 15 | 16 | ## Running crunchy-proxy 17 | 18 | First make sure that both the 'master' and 'replica' PostgreSQL nodes are 19 | running. Then it is necessary to initialize the the database for use with 20 | pgbench. 21 | 22 | These tests assume that these nodes are available at the following domains: 23 | 24 | * master.crunchy.lab 25 | * replica.crunchy.lab 26 | 27 | Initialize database: 28 | 29 | ``` 30 | $> cd tests/pgbench 31 | $> ./init_tests.sh 32 | ``` 33 | 34 | After initializing the test database, start up the proxy using the provided 35 | configuration. Running the proxy from source requires doing so from the 36 | project's root directory. 37 | 38 | Run the proxy: 39 | 40 | ``` 41 | $> go run main.go start --config=./tests/pgbench/config.yaml 42 | ``` 43 | 44 | ## Running the tests 45 | 46 | Tests must be run from the `tests/pgbench` directory. 47 | 48 | For load testing: 49 | 50 | ``` 51 | $> cd tests/pgbench 52 | $> ./run-load-tests.sh 53 | ``` 54 | 55 | For concurrency testing: 56 | 57 | ``` 58 | $> cd tests/pgbench 59 | $> ./run-concurrency-tests.sh 60 | ``` 61 | -------------------------------------------------------------------------------- /tests/pgbench/concurrency-test.sql: -------------------------------------------------------------------------------- 1 | select now(); 2 | /* read */ select now(); 3 | insert into proxytest values (1, 'foo', 'bar'); 4 | /* read */ select count(*) from proxytest; 5 | update proxytest set name = 'doo' where id = 1; 6 | delete from proxytest where id = 1; 7 | insert into proxytest values (2, 'aaa', 'bbb'); 8 | /* read */ select value from proxytest; 9 | delete from proxytest; 10 | -------------------------------------------------------------------------------- /tests/pgbench/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | proxy: 3 | hostport: 127.0.0.1:5432 4 | admin: 5 | hostport: 127.0.0.1:8000 6 | 7 | nodes: 8 | master: 9 | hostport: 192.168.56.100:5432 10 | role: master 11 | metadata: {} 12 | replica1: 13 | hostport: 192.168.56.101:5432 14 | role: replica 15 | metadata: {} 16 | 17 | credentials: 18 | username: postgres 19 | database: proxydb 20 | password: password 21 | options: 22 | application_name: "proxypool_pgbench" 23 | client_encoding: "UTF8" 24 | extra_float_digits: "2" 25 | 26 | ssl: 27 | enable: false 28 | sslmode: disable 29 | 30 | pool: 31 | capacity: 5 32 | 33 | healthcheck: 34 | delay: 60 35 | query: select now(); 36 | -------------------------------------------------------------------------------- /tests/pgbench/init-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2016 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | echo "starting pgbench load test..." 17 | 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 19 | export PGPASSFILE=$DIR/pgpass 20 | 21 | PORT=5432 22 | PROXY_HOST=localhost 23 | MASTER_HOST=master.crunchy.lab 24 | PG_USER=postgres 25 | DATABASE=proxydb 26 | 27 | echo 28 | psql -h $MASTER_HOST -p $PORT -U $PG_USER -c "drop database $DATABASE;" postgres 29 | psql -h $MASTER_HOST -p $PORT -U $PG_USER -c "create database $DATABASE;" postgres 30 | pgbench -h $MASTER_HOST -p $PORT -U $PG_USER -i $DATABASE 31 | -------------------------------------------------------------------------------- /tests/pgbench/pgpass: -------------------------------------------------------------------------------- 1 | *:*:*:*:password 2 | 3 | -------------------------------------------------------------------------------- /tests/pgbench/run-concurrency-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2016 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | echo "starting pgbench load test..." 17 | 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 19 | export PGPASSFILE=$DIR/pgpass 20 | 21 | PORT=5432 22 | HOST=localhost 23 | 24 | echo "refresh the proxydb database.." 25 | psql -h $HOST -p $PORT -U postgres -c 'drop database proxydb;' postgres 26 | psql -h $HOST -p $PORT -U postgres -c 'create database proxydb;' postgres 27 | pgbench -h master.crunchy.lab -p 5432 -U postgres -i proxydb 28 | psql -h $HOST -p $PORT -U postgres -c 'create table proxytest (id int, name varchar(20), value varchar(20));' proxydb 29 | 30 | echo "start the load test..." 31 | 32 | pgbench \ 33 | -h $HOST \ 34 | -p $PORT \ 35 | -U postgres \ 36 | -f $DIR/concurrency-test.sql \ 37 | -c 4 \ 38 | -t 100 proxydb 39 | 40 | echo "concurrency test ends." 41 | -------------------------------------------------------------------------------- /tests/pgbench/run-simple-load-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2016 Crunchy Data Solutions, Inc. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | echo "starting pgbench load test..." 17 | 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 19 | export PGPASSFILE=$DIR/pgpass 20 | 21 | PORT=5432 22 | PROXY_HOST=localhost 23 | MASTER_HOST=master.crunchy.lab 24 | 25 | echo "Initialize the 'proxydb' database..." 26 | pgbench -h $MASTER_HOST -p $PORT -U postgres -i proxydb 27 | 28 | echo "start the load test..." 29 | 30 | pgbench \ 31 | -h $PROXY_HOST \ 32 | -p $PORT \ 33 | -U postgres \ 34 | -f $DIR/simple-load-test.sql \ 35 | -c 10 \ 36 | --no-vacuum \ 37 | -t 10000 proxydb 38 | 39 | echo "load test ends." 40 | -------------------------------------------------------------------------------- /tests/pgbench/simple-load-test.sql: -------------------------------------------------------------------------------- 1 | select now(); 2 | /* read */ select now(); 3 | -------------------------------------------------------------------------------- /tests/psql/txn_test.sql: -------------------------------------------------------------------------------- 1 | begin /* start */; 2 | select now(); 3 | commit /* finish */; 4 | -------------------------------------------------------------------------------- /tests/retry_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "bytes" 18 | "database/sql" 19 | "log" 20 | "net/http" 21 | "os/exec" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestRetry(t *testing.T) { 27 | const SLEEP_TIME = 6 28 | log.SetFlags(log.Ltime | log.Lmicroseconds) 29 | log.Println("TestRetry was called") 30 | var startTime = time.Now() 31 | conn, err := Connect() 32 | defer conn.Close() 33 | if err != nil { 34 | t.FailNow() 35 | } 36 | 37 | //read the proxy event stream which equates to a healthcheck 38 | //has just been performed...after which we will have a retry test 39 | //window 40 | _, err = http.Get("http://localhost:10000/api/stream") 41 | if err != nil { 42 | t.FailNow() 43 | } 44 | log.Println("after the GET") 45 | 46 | //shut down the replica 47 | var cmdStdout, cmdStderr bytes.Buffer 48 | var cmd *exec.Cmd 49 | cmd = exec.Command("docker", "stop", "replica") 50 | cmd.Stdout = &cmdStdout 51 | cmd.Stderr = &cmdStderr 52 | err = cmd.Run() 53 | if err != nil { 54 | log.Println("docker stop stdout=" + cmdStdout.String()) 55 | log.Println("docker stop stderr=" + cmdStderr.String()) 56 | t.FailNow() 57 | } 58 | log.Println("docker stop stdout=" + cmdStdout.String()) 59 | 60 | //sleep a bit to give the replica time to stop 61 | log.Println("sleeping to let replica shutdown") 62 | time.Sleep(time.Duration(SLEEP_TIME) * time.Second) 63 | 64 | var timestamp string 65 | log.Println("performing a read query with replica down but with hc showing up") 66 | err = conn.QueryRow("/* read */ select text(now())").Scan(×tamp) 67 | switch { 68 | case err == sql.ErrNoRows: 69 | log.Println("no rows returned") 70 | t.FailNow() 71 | case err != nil: 72 | log.Println(err.Error()) 73 | t.FailNow() 74 | default: 75 | log.Println(timestamp + " was returned") 76 | } 77 | 78 | var endTime = time.Since(startTime) 79 | 80 | log.Println("before starting docker..") 81 | cmd = exec.Command("docker", "start", "replica") 82 | err = cmd.Run() 83 | if err != nil { 84 | log.Println("docker start stdout=" + cmdStdout.String()) 85 | log.Println("docker start stderr=" + cmdStderr.String()) 86 | t.FailNow() 87 | } 88 | log.Println("docker start stdout=" + cmdStdout.String()) 89 | //sleep a bit to give the replica time to restart 90 | log.Println("sleeping to let replica restart") 91 | time.Sleep(time.Duration(SLEEP_TIME) * time.Second) 92 | 93 | log.Printf("Duration %s\n", endTime) 94 | 95 | } 96 | -------------------------------------------------------------------------------- /tests/select_noanno_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "database/sql" 18 | "log" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestSelectNoAnno(t *testing.T) { 24 | log.SetFlags(log.Ltime | log.Lmicroseconds) 25 | log.Println("TestSelect was called") 26 | var startTime = time.Now() 27 | conn, err := Connect() 28 | defer conn.Close() 29 | if err != nil { 30 | t.FailNow() 31 | } 32 | 33 | var timestamp string 34 | err = conn.QueryRow("select text(now())").Scan(×tamp) 35 | switch { 36 | case err == sql.ErrNoRows: 37 | log.Println("no rows returned") 38 | t.FailNow() 39 | case err != nil: 40 | log.Println(err.Error()) 41 | t.FailNow() 42 | default: 43 | log.Println(timestamp + " was returned") 44 | } 45 | 46 | var endTime = time.Since(startTime) 47 | log.Printf("Duration %s\n", endTime) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/select_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "database/sql" 18 | "log" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestSelect(t *testing.T) { 24 | log.SetFlags(log.Ltime | log.Lmicroseconds) 25 | log.Println("TestSelect was called") 26 | var startTime = time.Now() 27 | conn, err := Connect() 28 | defer conn.Close() 29 | if err != nil { 30 | t.FailNow() 31 | } 32 | 33 | var timestamp string 34 | err = conn.QueryRow("/* read */ select text(now())").Scan(×tamp) 35 | switch { 36 | case err == sql.ErrNoRows: 37 | log.Println("no rows returned") 38 | t.FailNow() 39 | case err != nil: 40 | log.Println(err.Error()) 41 | t.FailNow() 42 | default: 43 | log.Println(timestamp + " was returned") 44 | } 45 | 46 | var endTime = time.Since(startTime) 47 | log.Printf("Duration %s\n", endTime) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/txn_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package tests 15 | 16 | import ( 17 | "database/sql" 18 | "log" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestTxn(t *testing.T) { 24 | log.SetFlags(log.Ltime | log.Lmicroseconds) 25 | log.Println("TestTxn was called") 26 | var startTime = time.Now() 27 | conn, err := Connect() 28 | defer conn.Close() 29 | if err != nil { 30 | t.FailNow() 31 | } 32 | 33 | var timestamp string 34 | _, err = conn.Exec("/* start */ begin") 35 | err = conn.QueryRow("/* read */ select text(now())").Scan(×tamp) 36 | switch { 37 | case err == sql.ErrNoRows: 38 | log.Println("no rows returned") 39 | t.FailNow() 40 | case err != nil: 41 | log.Println(err.Error()) 42 | t.FailNow() 43 | default: 44 | log.Println(timestamp + " was returned") 45 | } 46 | _, err = conn.Exec("/* finish */ commit") 47 | 48 | var endTime = time.Since(startTime) 49 | log.Printf("Duration %s\n", endTime) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /util/grpcutil/grpcutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package grpcutil 16 | 17 | import ( 18 | "io" 19 | "strings" 20 | 21 | "golang.org/x/net/context" 22 | 23 | "github.com/pkg/errors" 24 | "google.golang.org/grpc" 25 | "google.golang.org/grpc/codes" 26 | "google.golang.org/grpc/transport" 27 | ) 28 | 29 | func IsClosedConnection(err error) bool { 30 | err = errors.Cause(err) 31 | 32 | if err == context.Canceled || 33 | grpc.Code(err) == codes.Canceled || 34 | grpc.Code(err) == codes.Unavailable || 35 | grpc.ErrorDesc(err) == grpc.ErrClientConnClosing.Error() || 36 | strings.Contains(err.Error(), "is closing") || 37 | strings.Contains(err.Error(), "tls: use of closed connection") || 38 | strings.Contains(err.Error(), "use of closed network connection") || 39 | strings.Contains(err.Error(), io.ErrClosedPipe.Error()) || 40 | strings.Contains(err.Error(), io.EOF.Error()) { 41 | return true 42 | } 43 | 44 | if streamErr, ok := err.(transport.StreamError); ok && streamErr.Code == codes.Canceled { 45 | return true 46 | } 47 | 48 | return false 49 | } 50 | -------------------------------------------------------------------------------- /util/grpcutil/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package grpcutil 16 | 17 | import ( 18 | "fmt" 19 | 20 | "google.golang.org/grpc/grpclog" 21 | ) 22 | 23 | func init() { 24 | grpclog.SetLogger(&logger{}) 25 | } 26 | 27 | type logger struct{} 28 | 29 | var _ grpclog.Logger = (*logger)(nil) 30 | 31 | func (*logger) Fatal(args ...interface{}) { 32 | fmt.Println("Fatal", args) 33 | } 34 | 35 | func (*logger) Fatalf(format string, args ...interface{}) { 36 | fmt.Println("Fatalf", args) 37 | } 38 | 39 | func (*logger) Fatalln(args ...interface{}) { 40 | fmt.Println("Fatalln", args) 41 | } 42 | 43 | func (*logger) Print(args ...interface{}) { 44 | fmt.Println("Print", args) 45 | } 46 | 47 | func (*logger) Printf(format string, args ...interface{}) { 48 | grpclog.Printf(format, args) 49 | } 50 | 51 | func (*logger) Println(args ...interface{}) { 52 | fmt.Println("Println", args) 53 | } 54 | -------------------------------------------------------------------------------- /util/log/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Crunchy Data Solutions, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package log 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/Sirupsen/logrus" 21 | ) 22 | 23 | var levels = []string{ 24 | "debug", 25 | "info", 26 | "error", 27 | "fatal", 28 | } 29 | 30 | func init() { 31 | logrus.SetFormatter(&logrus.TextFormatter{ 32 | ForceColors: true, 33 | FullTimestamp: true, 34 | }) 35 | logrus.SetOutput(os.Stdout) 36 | } 37 | 38 | func Debug(msg string) { 39 | logrus.Debug(msg) 40 | } 41 | 42 | func Debugf(format string, args ...interface{}) { 43 | logrus.Debugf(format, args...) 44 | } 45 | 46 | func Info(msg string) { 47 | logrus.Info(msg) 48 | } 49 | 50 | func Infof(format string, args ...interface{}) { 51 | logrus.Infof(format, args...) 52 | } 53 | 54 | func Error(msg string) { 55 | logrus.Error(msg) 56 | } 57 | 58 | func Errorf(format string, args ...interface{}) { 59 | logrus.Errorf(format, args...) 60 | } 61 | 62 | func Fatal(msg string) { 63 | logrus.Fatal(msg) 64 | } 65 | 66 | func Fatalf(format string, args ...interface{}) { 67 | logrus.Fatalf(format, args...) 68 | } 69 | 70 | func SetLevel(level string) { 71 | logrusLevel, err := logrus.ParseLevel(level) 72 | 73 | if err != nil { 74 | logrus.Fatalf("\"%s\" is not a valid logging level", level) 75 | } 76 | 77 | logrus.SetLevel(logrusLevel) 78 | } 79 | --------------------------------------------------------------------------------