├── .agola └── config.jsonnet ├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ ├── enhancement.md │ └── support.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── DCO ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── common.go ├── keeper │ ├── cmd │ │ ├── keeper.go │ │ ├── keeper_test.go │ │ └── metrics.go │ └── main.go ├── proxy │ ├── cmd │ │ └── proxy.go │ └── main.go ├── sentinel │ ├── cmd │ │ ├── metrics.go │ │ ├── sentinel.go │ │ └── sentinel_test.go │ └── main.go ├── stolonctl │ ├── cmd │ │ ├── clusterdata.go │ │ ├── clusterdata_test.go │ │ ├── failkeeper.go │ │ ├── init.go │ │ ├── internal │ │ │ └── mock │ │ │ │ └── register │ │ │ │ └── discovery.go │ │ ├── promote.go │ │ ├── register.go │ │ ├── register │ │ │ ├── cluster.go │ │ │ ├── cluster_test.go │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── discovery.go │ │ │ ├── discovery_test.go │ │ │ ├── serviceinfo.go │ │ │ └── serviceinfo_test.go │ │ ├── register_test.go │ │ ├── removekeeper.go │ │ ├── spec.go │ │ ├── status.go │ │ ├── stolonctl.go │ │ └── update.go │ └── main.go └── version.go ├── doc ├── README.md ├── architecture.md ├── architecture.png ├── architecture.svg ├── architecture_small.png ├── cluster_spec.md ├── commands │ ├── stolon-keeper.md │ ├── stolon-proxy.md │ ├── stolon-sentinel.md │ ├── stolonctl.md │ ├── stolonctl_clusterdata.md │ ├── stolonctl_clusterdata_read.md │ ├── stolonctl_clusterdata_write.md │ ├── stolonctl_failkeeper.md │ ├── stolonctl_init.md │ ├── stolonctl_promote.md │ ├── stolonctl_register.md │ ├── stolonctl_removekeeper.md │ ├── stolonctl_spec.md │ ├── stolonctl_status.md │ ├── stolonctl_update.md │ └── stolonctl_version.md ├── commands_invocation.md ├── custom_pg_hba_entries.md ├── faq.md ├── forcefailover.md ├── initialization.md ├── manual_switchover.md ├── pg_rewind.md ├── pitr.md ├── pitr_wal-e.md ├── pitr_wal-g.md ├── postgres_parameters.md ├── service_discovery.md ├── simplecluster.md ├── ssl.md ├── standbycluster.md ├── standbycluster.svg ├── standbycluster_small.png ├── standbycluster_standalone.svg ├── standbycluster_standalone_small.png ├── stolon_service_discovery.png ├── stolonctl.md ├── syncrepl.md ├── twonodes.md ├── twonodes_keepalived.svg ├── twonodes_keepalived_small.png ├── twonodes_lb.svg ├── twonodes_lb_small.png ├── unofficial_packages.md └── upgrade.md ├── examples ├── kubernetes │ ├── README.md │ ├── image │ │ └── docker │ │ │ └── Dockerfile │ ├── postgresql_upgrade.md │ ├── role-binding.yaml │ ├── role.yaml │ ├── secret.yaml │ ├── stolon-keeper.yaml │ ├── stolon-proxy-service.yaml │ ├── stolon-proxy.yaml │ └── stolon-sentinel.yaml ├── openshift │ └── README.md └── swarm │ ├── README.md │ ├── docker-compose-etcd.yml │ ├── docker-compose-pg.yml │ └── etc │ └── secrets │ ├── pgsql │ └── pgsql_repl ├── go.mod ├── go.sum ├── internal ├── cluster │ ├── cluster.go │ ├── cluster_test.go │ ├── member.go │ └── v0 │ │ ├── clusterview.go │ │ ├── config.go │ │ ├── config_test.go │ │ └── member.go ├── common │ ├── common.go │ ├── common_test.go │ └── tls.go ├── flagutil │ └── env.go ├── log │ └── log.go ├── mock │ ├── postgresql │ │ └── postgresql.go │ └── store │ │ └── store.go ├── postgresql │ ├── connstring.go │ ├── control.go │ ├── postgresql.go │ ├── utils.go │ └── utils_test.go ├── store │ ├── etcdv3.go │ ├── k8s.go │ ├── kvbacked.go │ ├── libkv.go │ └── store.go ├── timer │ ├── timer.go │ ├── timer_fallback.go │ └── timer_linux.go └── util │ ├── k8s.go │ ├── slice.go │ ├── slice_test.go │ └── user.go ├── licenses └── LICENSE-BSD-3-Clause ├── logos ├── stolon-color.png └── stolon-logo.svg ├── scripts ├── agola-k8s.sh ├── build-binary ├── gen_commands_doc.go ├── gen_commands_doc.sh ├── git-version.sh ├── readlinkdashf.sh └── release.sh ├── test └── tests └── integration ├── config_test.go ├── ha_test.go ├── init_test.go ├── pitr_test.go ├── proxy_test.go ├── sentinel_test.go ├── standby_test.go └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | /release/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug on Stolon 4 | labels: bug 5 | 6 | --- 7 | 8 | 15 | 16 | 17 | **What happened**: 18 | 19 | **What you expected to happen**: 20 | 21 | **How to reproduce it (as minimally and precisely as possible)**: 22 | 23 | **Anything else we need to know?**: 24 | 25 | **Environment**: 26 | - Stolon version: 27 | - Stolon running environment (if useful to understand the bug): 28 | - Others: 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Stolon Community Forum 4 | url: https://talk.stolon.io 5 | about: For general discussion about using and developing Stolon 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Suggest an enhancement to Stolon 4 | labels: enhancement 5 | 6 | --- 7 | 12 | 13 | **What would you like to be added**: 14 | 15 | **Why is this needed**: 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Request 3 | about: Support request or question related to Stolon 4 | --- 5 | 6 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs save files 2 | *~ 3 | \#*\# 4 | .\#* 5 | 6 | # Vim-related files 7 | [._]*.s[a-w][a-z] 8 | [._]s[a-w][a-z] 9 | *.un~ 10 | Session.vim 11 | .netrwhist 12 | .idea/ 13 | 14 | # 15 | bin/ 16 | /release/ 17 | 18 | .idea 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sgotti@sorint.it. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJDIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 2 | 3 | # change to project dir so we can express all as relative paths 4 | $(shell cd $(PROJDIR)) 5 | 6 | REPO_PATH=github.com/sorintlab/stolon 7 | 8 | VERSION ?= $(shell scripts/git-version.sh) 9 | 10 | LD_FLAGS="-w -X $(REPO_PATH)/cmd.Version=$(VERSION)" 11 | 12 | $(shell mkdir -p bin ) 13 | 14 | 15 | .PHONY: all 16 | all: build 17 | 18 | .PHONY: build 19 | build: sentinel keeper proxy stolonctl 20 | 21 | .PHONY: test 22 | test: build 23 | ./test 24 | 25 | .PHONY: sentinel keeper proxy stolonctl docker 26 | 27 | keeper: 28 | GO111MODULE=on go build -ldflags $(LD_FLAGS) -o $(PROJDIR)/bin/stolon-keeper $(REPO_PATH)/cmd/keeper 29 | 30 | sentinel: 31 | CGO_ENABLED=0 GO111MODULE=on go build -ldflags $(LD_FLAGS) -o $(PROJDIR)/bin/stolon-sentinel $(REPO_PATH)/cmd/sentinel 32 | 33 | proxy: 34 | CGO_ENABLED=0 GO111MODULE=on go build -ldflags $(LD_FLAGS) -o $(PROJDIR)/bin/stolon-proxy $(REPO_PATH)/cmd/proxy 35 | 36 | stolonctl: 37 | CGO_ENABLED=0 GO111MODULE=on go build -ldflags $(LD_FLAGS) -o $(PROJDIR)/bin/stolonctl $(REPO_PATH)/cmd/stolonctl 38 | 39 | .PHONY: docker 40 | docker: 41 | if [ -z $${PGVERSION} ]; then echo 'PGVERSION is undefined'; exit 1; fi; \ 42 | if [ -z $${TAG} ]; then echo 'TAG is undefined'; exit 1; fi; \ 43 | docker build --build-arg PGVERSION=${PGVERSION} -t ${TAG} -f examples/kubernetes/image/docker/Dockerfile . 44 | -------------------------------------------------------------------------------- /cmd/keeper/cmd/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "github.com/prometheus/client_golang/prometheus" 19 | "github.com/sorintlab/stolon/internal/common" 20 | ) 21 | 22 | var ( 23 | clusterdataLastValidUpdateSeconds = prometheus.NewGauge( 24 | prometheus.GaugeOpts{ 25 | Name: "stolon_keeper_clusterdata_last_valid_update_seconds", 26 | Help: "Last time we received a valid clusterdata from our store as seconds since unix epoch", 27 | }, 28 | ) 29 | targetRoleGauge = prometheus.NewGaugeVec( 30 | prometheus.GaugeOpts{ 31 | Name: "stolon_keeper_target_role", 32 | Help: "Keeper last requested target role", 33 | }, 34 | []string{"role"}, 35 | ) 36 | localRoleGauge = prometheus.NewGaugeVec( 37 | prometheus.GaugeOpts{ 38 | Name: "stolon_keeper_local_role", 39 | Help: "Keeper current local role", 40 | }, 41 | []string{"role"}, 42 | ) 43 | needsReloadGauge = prometheus.NewGauge( 44 | prometheus.GaugeOpts{ 45 | Name: "stolon_keeper_needs_reload", 46 | Help: "Set to 1 if Postgres requires reload", 47 | }, 48 | ) 49 | needsRestartGauge = prometheus.NewGauge( 50 | prometheus.GaugeOpts{ 51 | Name: "stolon_keeper_needs_restart", 52 | Help: "Set to 1 if Postgres requires restart", 53 | }, 54 | ) 55 | lastSyncSuccessSeconds = prometheus.NewGauge( 56 | prometheus.GaugeOpts{ 57 | Name: "stolon_keeper_last_sync_success_seconds", 58 | Help: "Last time we successfully synced our keeper", 59 | }, 60 | ) 61 | sleepInterval = prometheus.NewGauge( 62 | prometheus.GaugeOpts{ 63 | Name: "stolon_keeper_sleep_interval", 64 | Help: "Seconds to sleep between sync loops", 65 | }, 66 | ) 67 | shutdownSeconds = prometheus.NewGauge( 68 | prometheus.GaugeOpts{ 69 | Name: "stolon_keeper_shutdown_seconds", 70 | Help: "Shutdown time (received termination signal) since unix epoch in seconds", 71 | }, 72 | ) 73 | ) 74 | 75 | // setRole is a helper that controls the targetRole metric by setting only one of the 76 | // possible roles to 1 at any one time. 77 | func setRole(rg *prometheus.GaugeVec, role *common.Role) { 78 | for _, role := range common.Roles { 79 | rg.WithLabelValues(string(role)).Set(0) 80 | } 81 | 82 | if role != nil { 83 | rg.WithLabelValues(string(*role)).Set(1) 84 | } 85 | } 86 | 87 | func init() { 88 | prometheus.MustRegister(clusterdataLastValidUpdateSeconds) 89 | prometheus.MustRegister(targetRoleGauge) 90 | setRole(targetRoleGauge, nil) 91 | prometheus.MustRegister(localRoleGauge) 92 | setRole(localRoleGauge, nil) 93 | prometheus.MustRegister(needsReloadGauge) 94 | prometheus.MustRegister(needsRestartGauge) 95 | prometheus.MustRegister(lastSyncSuccessSeconds) 96 | prometheus.MustRegister(sleepInterval) 97 | prometheus.MustRegister(shutdownSeconds) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/keeper/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 | "github.com/sorintlab/stolon/cmd/keeper/cmd" 19 | ) 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 | "github.com/sorintlab/stolon/cmd/proxy/cmd" 19 | ) 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /cmd/sentinel/cmd/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "github.com/prometheus/client_golang/prometheus" 19 | ) 20 | 21 | var ( 22 | lastCheckSuccessSeconds = prometheus.NewGauge( 23 | prometheus.GaugeOpts{ 24 | Name: "stolon_sentinel_last_cluster_check_success_seconds", 25 | Help: "Last time we successfully performed a cluster check as seconds since unix epoch", 26 | }, 27 | ) 28 | // These metrics will be provided by the collector. They represent values 29 | // that shouldn't be updated as part of the sentinel code, but should be 30 | // gathered just prior to providing Prometheus with a measurement. 31 | isLeaderDesc = prometheus.NewDesc( 32 | "stolon_sentinel_is_leader", 33 | "Set to 1 if the sentinel is currently a leader", 34 | []string{}, 35 | nil, 36 | ) 37 | leaderCountDesc = prometheus.NewDesc( 38 | "stolon_sentinel_leader_count", 39 | "Number of times this sentinel has been elected as leader", 40 | []string{}, 41 | nil, 42 | ) 43 | ) 44 | 45 | // Register the static methods on the default Prometheus registry automatically 46 | func init() { 47 | prometheus.MustRegister(lastCheckSuccessSeconds) 48 | } 49 | 50 | func mustRegisterSentinelCollector(s *Sentinel) { 51 | prometheus.MustRegister( 52 | sentinelCollector{s}, 53 | ) 54 | } 55 | 56 | type sentinelCollector struct { 57 | *Sentinel 58 | } 59 | 60 | func (c sentinelCollector) Describe(ch chan<- *prometheus.Desc) { 61 | prometheus.DescribeByCollect(c, ch) 62 | } 63 | 64 | func (c sentinelCollector) Collect(ch chan<- prometheus.Metric) { 65 | var isLeaderValue float64 66 | isLeader, leaderCount := c.Sentinel.leaderInfo() 67 | if isLeader { 68 | isLeaderValue = 1 69 | } 70 | 71 | ch <- prometheus.MustNewConstMetric(isLeaderDesc, prometheus.GaugeValue, isLeaderValue) 72 | ch <- prometheus.MustNewConstMetric(leaderCountDesc, prometheus.GaugeValue, float64(leaderCount)) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/sentinel/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 | "github.com/sorintlab/stolon/cmd/sentinel/cmd" 19 | ) 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/clusterdata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "os" 24 | 25 | cmdcommon "github.com/sorintlab/stolon/cmd" 26 | "github.com/sorintlab/stolon/internal/cluster" 27 | "github.com/sorintlab/stolon/internal/store" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var cmdClusterData = &cobra.Command{ 33 | Use: "clusterdata", 34 | Short: "Manage current cluster data", 35 | } 36 | 37 | type clusterdataReadOptions struct { 38 | pretty bool 39 | } 40 | 41 | var readClusterdataOpts clusterdataReadOptions 42 | 43 | type clusterdataWriteOptions struct { 44 | file string 45 | forceYes bool 46 | } 47 | 48 | var writeClusterdataOpts clusterdataWriteOptions 49 | 50 | var cmdReadClusterData = &cobra.Command{ 51 | Use: "read", 52 | Run: readClusterdata, 53 | Short: "Retrieve the current cluster data", 54 | } 55 | 56 | var cmdWriteClusterData = &cobra.Command{ 57 | Use: "write", 58 | Run: runWriteClusterdata, 59 | Short: "Write cluster data", 60 | } 61 | 62 | func init() { 63 | cmdReadClusterData.PersistentFlags().BoolVar(&readClusterdataOpts.pretty, "pretty", false, "pretty print") 64 | cmdClusterData.AddCommand(cmdReadClusterData) 65 | 66 | cmdWriteClusterData.PersistentFlags().StringVarP(&writeClusterdataOpts.file, "file", "f", "", "file containing the new cluster data") 67 | cmdWriteClusterData.PersistentFlags().BoolVarP(&writeClusterdataOpts.forceYes, "yes", "y", false, "don't ask for confirmation") 68 | cmdClusterData.AddCommand(cmdWriteClusterData) 69 | 70 | CmdStolonCtl.AddCommand(cmdClusterData) 71 | } 72 | 73 | func readClusterdata(cmd *cobra.Command, args []string) { 74 | e, err := cmdcommon.NewStore(&cfg.CommonConfig) 75 | if err != nil { 76 | die("%v", err) 77 | } 78 | 79 | cd, _, err := getClusterData(e) 80 | if err != nil { 81 | die("%v", err) 82 | } 83 | if cd.Cluster == nil { 84 | die("no cluster clusterdata available") 85 | } 86 | var clusterdataj []byte 87 | if readClusterdataOpts.pretty { 88 | clusterdataj, err = json.MarshalIndent(cd, "", "\t") 89 | if err != nil { 90 | die("failed to marshall clusterdata: %v", err) 91 | } 92 | } else { 93 | clusterdataj, err = json.Marshal(cd) 94 | if err != nil { 95 | die("failed to marshall clusterdata: %v", err) 96 | } 97 | } 98 | stdout("%s", clusterdataj) 99 | } 100 | 101 | func isSafeToWriteClusterData(store store.Store) error { 102 | if cd, _, err := store.GetClusterData(context.TODO()); err != nil { 103 | return err 104 | } else if cd != nil { 105 | if !writeClusterdataOpts.forceYes { 106 | return fmt.Errorf("WARNING: cluster data already available use --yes to override") 107 | } else { 108 | stdout("WARNING: The current cluster data will be removed") 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | func clusterData(data []byte) (*cluster.ClusterData, error) { 115 | cd := cluster.ClusterData{} 116 | err := json.Unmarshal(data, &cd) 117 | return &cd, err 118 | } 119 | 120 | func writeClusterdata(reader io.Reader, s store.Store) error { 121 | data, err := ioutil.ReadAll(reader) 122 | if err != nil { 123 | return fmt.Errorf("error while reading data: %v", err) 124 | } 125 | 126 | cd, err := clusterData(data) 127 | 128 | if err != nil { 129 | return fmt.Errorf("invalid cluster data: %v", err) 130 | } 131 | 132 | if err = isSafeToWriteClusterData(s); err != nil { 133 | return err 134 | } 135 | 136 | err = s.PutClusterData(context.TODO(), cd) 137 | 138 | if err != nil { 139 | return fmt.Errorf("failed to write cluster data into new store %v", err) 140 | } 141 | stdout("successfully wrote cluster data into the new store") 142 | return nil 143 | } 144 | 145 | func runWriteClusterdata(_ *cobra.Command, _ []string) { 146 | var reader io.Reader 147 | if writeClusterdataOpts.file == "" || writeClusterdataOpts.file == "-" { 148 | reader = os.Stdin 149 | } else { 150 | file, err := os.Open(writeClusterdataOpts.file) 151 | if err != nil { 152 | die("cannot read file: %v", err) 153 | } 154 | defer file.Close() 155 | reader = file 156 | } 157 | s, err := cmdcommon.NewStore(&cfg.CommonConfig) 158 | if err != nil { 159 | die("failed to create new store %v", err) 160 | } 161 | if err := writeClusterdata(reader, s); err != nil { 162 | die("%v", err) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/failkeeper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | 20 | cmdcommon "github.com/sorintlab/stolon/cmd" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var failKeeperCmd = &cobra.Command{ 25 | Use: "failkeeper [keeper uid]", 26 | Short: `Force keeper as "temporarily" failed. The sentinel will compute a new clusterdata considering it as failed and then restore its state to the real one.`, 27 | Long: `Force keeper as "temporarily" failed. It's just a one shot operation, the sentinel will compute a new clusterdata considering the keeper as failed and then restore its state to the real one. For example, if the force failed keeper is a master, the sentinel will try to elect a new master. If no new master can be elected, the force failed keeper, if really healthy, will be re-elected as master`, 28 | Run: failKeeper, 29 | } 30 | 31 | func init() { 32 | CmdStolonCtl.AddCommand(failKeeperCmd) 33 | } 34 | 35 | func failKeeper(cmd *cobra.Command, args []string) { 36 | if len(args) > 1 { 37 | die("too many arguments") 38 | } 39 | 40 | if len(args) == 0 { 41 | die("keeper uid required") 42 | } 43 | 44 | keeperID := args[0] 45 | 46 | store, err := cmdcommon.NewStore(&cfg.CommonConfig) 47 | if err != nil { 48 | die("%v", err) 49 | } 50 | 51 | cd, pair, err := getClusterData(store) 52 | if err != nil { 53 | die("cannot get cluster data: %v", err) 54 | } 55 | if cd.Cluster == nil { 56 | die("no cluster spec available") 57 | } 58 | if cd.Cluster.Spec == nil { 59 | die("no cluster spec available") 60 | } 61 | 62 | newCd := cd.DeepCopy() 63 | keeperInfo := newCd.Keepers[keeperID] 64 | if keeperInfo == nil { 65 | die("keeper doesn't exist") 66 | } 67 | 68 | keeperInfo.Status.ForceFail = true 69 | 70 | _, err = store.AtomicPutClusterData(context.TODO(), newCd, pair) 71 | if err != nil { 72 | die("cannot update cluster data: %v", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "io/ioutil" 21 | "os" 22 | 23 | cmdcommon "github.com/sorintlab/stolon/cmd" 24 | "github.com/sorintlab/stolon/internal/cluster" 25 | "github.com/sorintlab/stolon/internal/common" 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | var cmdInit = &cobra.Command{ 30 | Use: "init", 31 | Run: initCluster, 32 | Short: "Initialize a new cluster", 33 | } 34 | 35 | type InitOptions struct { 36 | file string 37 | forceYes bool 38 | } 39 | 40 | var initOpts InitOptions 41 | 42 | func init() { 43 | cmdInit.PersistentFlags().StringVarP(&initOpts.file, "file", "f", "", "file contaning the new cluster spec") 44 | cmdInit.PersistentFlags().BoolVarP(&initOpts.forceYes, "yes", "y", false, "don't ask for confirmation") 45 | 46 | CmdStolonCtl.AddCommand(cmdInit) 47 | } 48 | 49 | func initCluster(cmd *cobra.Command, args []string) { 50 | if len(args) > 1 { 51 | die("too many arguments") 52 | } 53 | 54 | dataSupplied := false 55 | data := []byte{} 56 | switch len(args) { 57 | case 1: 58 | dataSupplied = true 59 | data = []byte(args[0]) 60 | case 0: 61 | if initOpts.file != "" { 62 | dataSupplied = true 63 | var err error 64 | if initOpts.file == "-" { 65 | data, err = ioutil.ReadAll(os.Stdin) 66 | if err != nil { 67 | die("cannot read from stdin: %v", err) 68 | } 69 | } else { 70 | data, err = ioutil.ReadFile(initOpts.file) 71 | if err != nil { 72 | die("cannot read file: %v", err) 73 | } 74 | } 75 | } 76 | } 77 | 78 | e, err := cmdcommon.NewStore(&cfg.CommonConfig) 79 | if err != nil { 80 | die("%v", err) 81 | } 82 | 83 | cd, _, err := e.GetClusterData(context.TODO()) 84 | if err != nil { 85 | die("cannot get cluster data: %v", err) 86 | } 87 | if cd != nil { 88 | stdout("WARNING: The current cluster data will be removed") 89 | } 90 | stdout("WARNING: The databases managed by the keepers will be overwritten depending on the provided cluster spec.") 91 | 92 | accepted := true 93 | if !initOpts.forceYes { 94 | accepted, err = askConfirmation("Are you sure you want to continue? [yes/no] ") 95 | if err != nil { 96 | die("%v", err) 97 | } 98 | } 99 | if !accepted { 100 | stdout("exiting") 101 | os.Exit(0) 102 | } 103 | 104 | _, _, err = e.GetClusterData(context.TODO()) 105 | if err != nil { 106 | die("cannot get cluster data: %v", err) 107 | } 108 | 109 | var cs *cluster.ClusterSpec 110 | if dataSupplied { 111 | if err := json.Unmarshal(data, &cs); err != nil { 112 | die("failed to unmarshal cluster spec: %v", err) 113 | } 114 | } else { 115 | // Define a new cluster spec with initMode "new" 116 | cs = &cluster.ClusterSpec{} 117 | cs.InitMode = cluster.ClusterInitModeP(cluster.ClusterInitModeNew) 118 | } 119 | 120 | if err := cs.Validate(); err != nil { 121 | die("invalid cluster spec: %v", err) 122 | } 123 | 124 | c := cluster.NewCluster(common.UID(), cs) 125 | cd = cluster.NewClusterData(c) 126 | 127 | // We ignore if cd has been modified between reading and writing 128 | if err := e.PutClusterData(context.TODO(), cd); err != nil { 129 | die("cannot update cluster data: %v", err) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/promote.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | "os" 20 | 21 | cmdcommon "github.com/sorintlab/stolon/cmd" 22 | "github.com/sorintlab/stolon/internal/cluster" 23 | "github.com/sorintlab/stolon/internal/store" 24 | 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var cmdPromote = &cobra.Command{ 29 | Use: "promote", 30 | Run: promote, 31 | Short: "Promotes a standby cluster to a primary cluster", 32 | } 33 | 34 | func init() { 35 | cmdPromote.PersistentFlags().BoolVarP(&initOpts.forceYes, "yes", "y", false, "don't ask for confirmation") 36 | 37 | CmdStolonCtl.AddCommand(cmdPromote) 38 | } 39 | 40 | func promote(cmd *cobra.Command, args []string) { 41 | if len(args) > 0 { 42 | die("too many arguments") 43 | } 44 | 45 | e, err := cmdcommon.NewStore(&cfg.CommonConfig) 46 | if err != nil { 47 | die("%v", err) 48 | } 49 | 50 | accepted := true 51 | if !initOpts.forceYes { 52 | accepted, err = askConfirmation("Are you sure you want to continue? [yes/no] ") 53 | if err != nil { 54 | die("%v", err) 55 | } 56 | } 57 | if !accepted { 58 | stdout("exiting") 59 | os.Exit(0) 60 | } 61 | 62 | retry := 0 63 | for retry < maxRetries { 64 | cd, pair, err := getClusterData(e) 65 | if err != nil { 66 | die("%v", err) 67 | } 68 | if cd.Cluster == nil { 69 | die("no cluster spec available") 70 | } 71 | if cd.Cluster.Spec == nil { 72 | die("no cluster spec available") 73 | } 74 | 75 | ds := cd.Cluster.DefSpec() 76 | if *ds.Role == cluster.ClusterRoleMaster { 77 | stderr("cluster spec role already set to master") 78 | os.Exit(0) 79 | } 80 | cd.Cluster.Spec.Role = cluster.ClusterRoleP(cluster.ClusterRoleMaster) 81 | 82 | if err = cd.Cluster.UpdateSpec(cd.Cluster.Spec); err != nil { 83 | die("Cannot update cluster spec: %v", err) 84 | } 85 | 86 | // retry if cd has been modified between reading and writing 87 | _, err = e.AtomicPutClusterData(context.TODO(), cd, pair) 88 | if err != nil { 89 | if err == store.ErrKeyModified { 90 | retry++ 91 | continue 92 | } 93 | die("cannot update cluster data: %v", err) 94 | } 95 | break 96 | } 97 | if retry == maxRetries { 98 | die("failed to update cluster data after %d retries", maxRetries) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/cluster.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 register 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | 21 | "github.com/sorintlab/stolon/internal/cluster" 22 | "github.com/sorintlab/stolon/internal/store" 23 | ) 24 | 25 | // Cluster type exposes necessary methods to find master and slave 26 | // from underlying store 27 | type Cluster struct { 28 | name string 29 | cd *cluster.ClusterData 30 | tagMasterAs Tags 31 | tagSlaveAs Tags 32 | } 33 | 34 | // NewCluster returns an new instance of Cluster 35 | func NewCluster(name string, rCfg Config, store store.Store) (*Cluster, error) { 36 | cd, _, err := store.GetClusterData(context.TODO()) 37 | 38 | if err != nil { 39 | return nil, err 40 | } else if cd == nil { 41 | return nil, errors.New("no cluster data available") 42 | } 43 | return &Cluster{name: name, cd: cd, tagMasterAs: NewTags(rCfg.TagMasterAs), tagSlaveAs: NewTags(rCfg.TagSlaveAs)}, nil 44 | } 45 | 46 | // ServiceInfos returns all the service information from the cluster data in underlying store 47 | func (c *Cluster) ServiceInfos() (ServiceInfos, error) { 48 | if c.cd.Cluster == nil { 49 | return nil, errors.New("cluster data not available") 50 | } 51 | 52 | serviceInfos := ServiceInfos{} 53 | master := c.cd.Cluster.Status.Master 54 | for uid, db := range c.cd.DBs { 55 | if db.Status.Healthy { 56 | tags := c.tagSlaveAs 57 | isMaster := false 58 | if uid == master { 59 | tags = c.tagMasterAs 60 | isMaster = true 61 | } 62 | info, err := NewServiceInfo(c.name, db, tags, isMaster) 63 | if err != nil { 64 | return nil, err 65 | } 66 | serviceInfos[uid] = *info 67 | } 68 | } 69 | 70 | return serviceInfos, nil 71 | } 72 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 | 14 | package register 15 | 16 | import ( 17 | "fmt" 18 | "net/url" 19 | "strings" 20 | 21 | "github.com/hashicorp/consul/api" 22 | ) 23 | 24 | // Config represents necessary configurations which can passed 25 | // for registering master and slave info for service discovery 26 | type Config struct { 27 | Backend string 28 | Endpoints string 29 | Token string 30 | TLSAddress string 31 | TLSCAFile string 32 | TLSCAPath string 33 | TLSCertFile string 34 | TLSKeyFile string 35 | TLSInsecureSkipVerify bool 36 | SleepInterval int 37 | TagMasterAs string 38 | TagSlaveAs string 39 | RegisterMaster bool 40 | } 41 | 42 | // Validate returns nil if the config is valid, else returns error with 43 | // appropriate reason 44 | func (config *Config) Validate() error { 45 | switch config.Backend { 46 | case "consul": 47 | addresses := strings.Split(config.Endpoints, ",") 48 | if len(addresses) != 1 { 49 | return fmt.Errorf("consul does not support multiple endpoints: %s", config.Endpoints) 50 | } 51 | _, err := url.Parse(config.Endpoints) 52 | return err 53 | default: 54 | return fmt.Errorf("unknown register backend: %q", config.Backend) 55 | } 56 | } 57 | 58 | // ConsulConfig returns consul.api.ConsulConfig if register endpoint is valid consul url 59 | // else will return error with appropriate reason 60 | func (config *Config) ConsulConfig() (*api.Config, error) { 61 | url, err := url.Parse(config.Endpoints) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return &api.Config{ 66 | Address: url.Host, 67 | Scheme: url.Scheme, 68 | TLSConfig: api.TLSConfig{ 69 | Address: config.TLSAddress, 70 | CAFile: config.TLSCAFile, 71 | CAPath: config.TLSCAPath, 72 | CertFile: config.TLSCertFile, 73 | KeyFile: config.TLSKeyFile, 74 | InsecureSkipVerify: config.TLSInsecureSkipVerify, 75 | }, 76 | }, nil 77 | } 78 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 | 14 | package register 15 | 16 | import "testing" 17 | 18 | func TestRegisterConfig(t *testing.T) { 19 | t.Run("validate", func(t *testing.T) { 20 | t.Run("should check for consul register backend", func(t *testing.T) { 21 | config := Config{Backend: "something other than consul"} 22 | err := config.Validate() 23 | 24 | if err == nil || err.Error() != "unknown register backend: \"something other than consul\"" { 25 | t.Errorf("expected unknown register backend but got %s", err.Error()) 26 | } 27 | }) 28 | 29 | t.Run("should not return any error if all valid configurations are specified", func(t *testing.T) { 30 | config := Config{Backend: "consul"} 31 | err := config.Validate() 32 | 33 | if err != nil { 34 | t.Errorf("expected no error but got '%v'", err.Error()) 35 | } 36 | }) 37 | 38 | t.Run("should not support multiple addresses", func(t *testing.T) { 39 | config := Config{Backend: "consul", Endpoints: "http://127.0.0.1:8500,http://127.0.0.2:8500"} 40 | err := config.Validate() 41 | 42 | if err == nil || err.Error() != "consul does not support multiple endpoints: http://127.0.0.1:8500,http://127.0.0.2:8500" { 43 | t.Errorf("expected unknown register backend but got %s", err.Error()) 44 | } 45 | }) 46 | }) 47 | 48 | t.Run("config", func(t *testing.T) { 49 | t.Run("should return config", func(t *testing.T) { 50 | c := Config{ 51 | Backend: "consul", 52 | Endpoints: "https://127.0.0.1:8500", 53 | TLSAddress: "address", 54 | TLSCAFile: "ca-file", 55 | TLSCAPath: "ca-path", 56 | TLSCertFile: "cert-file", 57 | TLSKeyFile: "key-file", 58 | TLSInsecureSkipVerify: true, 59 | } 60 | config, err := c.ConsulConfig() 61 | 62 | if err != nil { 63 | t.Errorf("expected error to be nil but got %s", err.Error()) 64 | } 65 | 66 | if config.Address != "127.0.0.1:8500" { 67 | t.Errorf("expected address to be %s but got %s", c.Endpoints, config.Address) 68 | } 69 | 70 | if config.Scheme != "https" { 71 | t.Errorf("expected address to be https but got %s", config.Scheme) 72 | } 73 | 74 | if config.TLSConfig.Address != "address" { 75 | t.Errorf("expected tls address to be address but got %s", config.TLSConfig.Address) 76 | } 77 | 78 | if config.TLSConfig.CAFile != "ca-file" { 79 | t.Errorf("expected tlsCaFile to be ca-file but got %s", config.TLSConfig.CAFile) 80 | } 81 | 82 | if config.TLSConfig.CAPath != "ca-path" { 83 | t.Errorf("expected tlsCaPath to be ca-path but got %s", config.TLSConfig.CAPath) 84 | } 85 | 86 | if config.TLSConfig.CertFile != "cert-file" { 87 | t.Errorf("expected tlsCertFile to be cert-file but got %s", config.TLSConfig.CertFile) 88 | } 89 | 90 | if config.TLSConfig.KeyFile != "key-file" { 91 | t.Errorf("expected tlsKeyFile to be key-file but got %s", config.TLSConfig.KeyFile) 92 | } 93 | 94 | if config.TLSConfig.InsecureSkipVerify != true { 95 | t.Errorf("expected tlsInsecureSkipVerify to be true but got %v", config.TLSConfig.InsecureSkipVerify) 96 | } 97 | }) 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/discovery.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 register 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/hashicorp/consul/api" 21 | ) 22 | 23 | // ServiceDiscovery helps to register service 24 | type ServiceDiscovery interface { 25 | Register(info *ServiceInfo) error 26 | Services(name string) (ServiceInfos, error) 27 | DeRegister(info *ServiceInfo) error 28 | } 29 | 30 | // NewServiceDiscovery creates a Discovery from registerBackend and registerEndpoints 31 | func NewServiceDiscovery(config *Config) (ServiceDiscovery, error) { 32 | switch config.Backend { 33 | case "consul": 34 | if apiConfig, err := config.ConsulConfig(); err != nil { 35 | return nil, err 36 | } else if client, err := api.NewClient(apiConfig); err != nil { 37 | return nil, err 38 | } else { 39 | agent := client.Agent() 40 | catalog := client.Catalog() 41 | return NewConsulServiceDiscovery(agent, catalog), nil 42 | } 43 | default: 44 | return nil, errors.New("register backend not supported") 45 | } 46 | } 47 | 48 | // ConsulServiceDiscovery helps to register service to consul 49 | type ConsulServiceDiscovery struct { 50 | agent ConsulAgent 51 | catalog ConsulCatalog 52 | } 53 | 54 | // ConsulAgent interface holds all the necessary methods to interact with consul agent 55 | type ConsulAgent interface { 56 | ServiceRegister(service *api.AgentServiceRegistration) error 57 | ServiceDeregister(serviceID string) error 58 | } 59 | 60 | type ConsulCatalog interface { 61 | Service(service, tag string, q *api.QueryOptions) ([]*api.CatalogService, *api.QueryMeta, error) 62 | } 63 | 64 | // NewConsulServiceDiscovery creates a new ConsulDiscovery 65 | func NewConsulServiceDiscovery(agent ConsulAgent, catalog ConsulCatalog) ServiceDiscovery { 66 | return &ConsulServiceDiscovery{agent: agent, catalog: catalog} 67 | } 68 | 69 | // Register registers the given service info to consul 70 | func (cd *ConsulServiceDiscovery) Register(info *ServiceInfo) error { 71 | return cd.agent.ServiceRegister(info.ConsulAgentServiceRegistration()) 72 | } 73 | 74 | // DeRegister de-registers the given service info to consul 75 | func (cd *ConsulServiceDiscovery) DeRegister(info *ServiceInfo) error { 76 | return cd.agent.ServiceDeregister(info.ID) 77 | } 78 | 79 | // Services returns the services registered in the consul 80 | func (cd *ConsulServiceDiscovery) Services(name string) (ServiceInfos, error) { 81 | services, _, err := cd.catalog.Service(name, "", nil) 82 | if err != nil { 83 | return nil, err 84 | } 85 | result := ServiceInfos{} 86 | for _, service := range services { 87 | if service != nil { 88 | consulService := NewServiceInfoFromConsulService(*service) 89 | result[service.ServiceID] = consulService 90 | } 91 | } 92 | return result, nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/discovery_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 register_test 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | 21 | "github.com/golang/mock/gomock" 22 | "github.com/hashicorp/consul/api" 23 | "github.com/sorintlab/stolon/cmd/stolonctl/cmd/internal/mock/register" 24 | "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" 25 | ) 26 | 27 | func TestNewServiceDiscovery(t *testing.T) { 28 | t.Run("should return consul service discovery", func(t *testing.T) { 29 | config := register.Config{Backend: "consul", Endpoints: "http://127.0.0.1"} 30 | sd, err := register.NewServiceDiscovery(&config) 31 | 32 | if err != nil { 33 | t.Errorf("expected error to be nil but was %s", err.Error()) 34 | } else if sd == nil { 35 | t.Errorf("expected service discovery not to be nil") 36 | } 37 | }) 38 | } 39 | 40 | func TestConsulServiceDiscovery(t *testing.T) { 41 | t.Run("should be able to register service info", func(t *testing.T) { 42 | ctrl := gomock.NewController(t) 43 | defer ctrl.Finish() 44 | 45 | client := mock_register.NewMockConsulAgent(ctrl) 46 | catalog := mock_register.NewMockConsulCatalog(ctrl) 47 | serviceDiscovery := register.NewConsulServiceDiscovery(client, catalog) 48 | expectedServiceInfo := register.ServiceInfo{ 49 | Name: "service", 50 | ID: "1", 51 | Port: 5432, 52 | Address: "127.0.0.1", 53 | Tags: []string{"tag"}, 54 | Check: register.HealthCheck{ 55 | Interval: "10s", 56 | TCP: "tcp", 57 | }, 58 | } 59 | 60 | client.EXPECT().ServiceRegister(expectedServiceInfo.ConsulAgentServiceRegistration()) 61 | 62 | err := serviceDiscovery.Register(&expectedServiceInfo) 63 | 64 | if err != nil { 65 | t.Errorf("expected error to be nil when registering service but got %s", err.Error()) 66 | } 67 | }) 68 | 69 | t.Run("should return error returned by http client", func(t *testing.T) { 70 | ctrl := gomock.NewController(t) 71 | defer ctrl.Finish() 72 | 73 | client := mock_register.NewMockConsulAgent(ctrl) 74 | catalog := mock_register.NewMockConsulCatalog(ctrl) 75 | serviceDiscovery := register.NewConsulServiceDiscovery(client, catalog) 76 | client.EXPECT().ServiceRegister(gomock.Any()).Return(errors.New("something went wrong")) 77 | 78 | err := serviceDiscovery.Register(®ister.ServiceInfo{}) 79 | 80 | if err == nil || err.Error() != "something went wrong" { 81 | t.Errorf("expected error to be something went wrong") 82 | } 83 | }) 84 | } 85 | 86 | func TestServices(t *testing.T) { 87 | t.Run("should return valid service infos", func(t *testing.T) { 88 | ctrl := gomock.NewController(t) 89 | defer ctrl.Finish() 90 | 91 | client := mock_register.NewMockConsulAgent(ctrl) 92 | catalog := mock_register.NewMockConsulCatalog(ctrl) 93 | serviceDiscovery := register.NewConsulServiceDiscovery(client, catalog) 94 | services := []*api.CatalogService{ 95 | {ServiceID: "masterUID", ServiceName: "test"}, 96 | {ServiceID: "slaveUID", ServiceName: "test"}, 97 | nil, 98 | } 99 | 100 | catalog.EXPECT().Service("test", "", nil).Return(services, nil, nil) 101 | 102 | infos, err := serviceDiscovery.Services("test") 103 | 104 | if err != nil { 105 | t.Errorf("expected error to be nil but was %s", err.Error()) 106 | } 107 | if len(infos) != 2 { 108 | t.Errorf("expected 2 services but got %d", len(infos)) 109 | } 110 | }) 111 | 112 | t.Run("should return error if service returns error", func(t *testing.T) { 113 | ctrl := gomock.NewController(t) 114 | defer ctrl.Finish() 115 | 116 | client := mock_register.NewMockConsulAgent(ctrl) 117 | catalog := mock_register.NewMockConsulCatalog(ctrl) 118 | serviceDiscovery := register.NewConsulServiceDiscovery(client, catalog) 119 | 120 | catalog.EXPECT().Service("test", "", nil).Return(nil, nil, errors.New("service error")) 121 | 122 | infos, err := serviceDiscovery.Services("test") 123 | 124 | if err == nil { 125 | t.Errorf("expected error to be service error but got nil") 126 | } 127 | if infos != nil { 128 | t.Errorf("expected 2 services but got %d", len(infos)) 129 | } 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/register/serviceinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Sorint.lab 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 | // 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 register 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "reflect" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/hashicorp/consul/api" 25 | "github.com/sorintlab/stolon/internal/cluster" 26 | ) 27 | 28 | // HealthCheck holds necessary information for performing 29 | // health check on the service 30 | type HealthCheck struct { 31 | TCP string 32 | Interval string 33 | } 34 | 35 | // Tags represents various way a service can be tagged 36 | type Tags []string 37 | 38 | // NewTags convert comma separated string into Tags 39 | func NewTags(from string) Tags { 40 | return Tags(strings.Split(from, ",")) 41 | } 42 | 43 | // Compare if two tags are equal 44 | func (t Tags) Compare(tags Tags) bool { 45 | return reflect.DeepEqual(t, tags) 46 | } 47 | 48 | // ServiceInfo holds the necessary information about a service 49 | // for service discovery 50 | type ServiceInfo struct { 51 | Name string 52 | Tags Tags 53 | ID string 54 | Address string 55 | IsMaster bool 56 | Port int 57 | Check HealthCheck 58 | } 59 | 60 | // ConsulAgentServiceRegistration returns AgentServiceRegistration 61 | func (info *ServiceInfo) ConsulAgentServiceRegistration() *api.AgentServiceRegistration { 62 | check := api.AgentServiceCheck{ 63 | TCP: info.Check.TCP, 64 | Interval: info.Check.Interval, 65 | } 66 | service := api.AgentServiceRegistration{ 67 | ID: info.ID, 68 | Name: info.Name, 69 | Address: info.Address, 70 | Tags: info.Tags, 71 | Port: info.Port, 72 | Check: &check, 73 | } 74 | return &service 75 | } 76 | 77 | // Compare if two ServiceInfo are equal 78 | func (info *ServiceInfo) Compare(target ServiceInfo) bool { 79 | return info.Name == target.Name && 80 | info.ID == target.ID && 81 | info.Address == target.Address && 82 | info.Port == target.Port && 83 | info.Tags.Compare(target.Tags) 84 | } 85 | 86 | // NewServiceInfo return new ServiceInfo from name, db and tags 87 | func NewServiceInfo(name string, db *cluster.DB, tags []string, isMaster bool) (*ServiceInfo, error) { 88 | port, err := strconv.Atoi(db.Status.Port) 89 | if err != nil { 90 | return nil, fmt.Errorf(fmt.Sprintf("invalid database port '%s' for %s with uid %s", db.Status.Port, name, db.UID)) 91 | } 92 | return &ServiceInfo{ 93 | Name: name, 94 | Tags: tags, 95 | Address: db.Status.ListenAddress, 96 | ID: db.UID, 97 | Port: port, 98 | IsMaster: isMaster, 99 | Check: HealthCheck{ 100 | TCP: net.JoinHostPort(db.Status.ListenAddress, db.Status.Port), 101 | Interval: "10s", 102 | }, 103 | }, nil 104 | } 105 | 106 | // NewServiceInfoFromConsulService returns ServiceInfo from consul service 107 | func NewServiceInfoFromConsulService(service api.CatalogService) ServiceInfo { 108 | return ServiceInfo{ 109 | Name: service.ServiceName, 110 | Tags: service.ServiceTags, 111 | ID: service.ServiceID, 112 | Port: service.ServicePort, 113 | Address: service.ServiceAddress, 114 | } 115 | } 116 | 117 | // ServiceInfos represents holds collection of ServiceInfo 118 | type ServiceInfos map[string]ServiceInfo 119 | 120 | // ServiceInfoStatus holds the services which are newly added / removed 121 | type ServiceInfoStatus struct { 122 | Added ServiceInfos 123 | Removed ServiceInfos 124 | } 125 | 126 | // Diff returns ServiceInfoStatus 127 | func (existing ServiceInfos) Diff(discovered ServiceInfos) ServiceInfoStatus { 128 | result := ServiceInfoStatus{Added: ServiceInfos{}, Removed: ServiceInfos{}} 129 | for id, existingInfo := range existing { 130 | if discoveredInfo, ok := discovered[id]; !ok { 131 | result.Added[id] = existingInfo 132 | } else { 133 | if !existingInfo.Compare(discoveredInfo) { 134 | result.Added[id] = existingInfo 135 | result.Removed[id] = discoveredInfo 136 | } 137 | } 138 | } 139 | for id, discoveredInfo := range discovered { 140 | if existingInfo, ok := existing[id]; !ok { 141 | result.Removed[id] = discoveredInfo 142 | } else { 143 | if !discoveredInfo.Compare(existingInfo) { 144 | result.Added[id] = existingInfo 145 | result.Removed[id] = discoveredInfo 146 | } 147 | } 148 | } 149 | return result 150 | } 151 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/removekeeper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | 20 | cmdcommon "github.com/sorintlab/stolon/cmd" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var removeKeeperCmd = &cobra.Command{ 25 | Use: "removekeeper [keeper uid]", 26 | Short: "Removes keeper from cluster data", 27 | Run: removeKeeper, 28 | } 29 | 30 | func init() { 31 | CmdStolonCtl.AddCommand(removeKeeperCmd) 32 | } 33 | 34 | func removeKeeper(_ *cobra.Command, args []string) { 35 | if len(args) > 1 { 36 | die("too many arguments") 37 | } 38 | 39 | if len(args) == 0 { 40 | die("keeper uid required") 41 | } 42 | 43 | keeperID := args[0] 44 | 45 | store, err := cmdcommon.NewStore(&cfg.CommonConfig) 46 | if err != nil { 47 | die("%v", err) 48 | } 49 | 50 | cd, pair, err := getClusterData(store) 51 | if err != nil { 52 | die("cannot get cluster data: %v", err) 53 | } 54 | if cd.Cluster == nil { 55 | die("no cluster spec available") 56 | } 57 | if cd.Cluster.Spec == nil { 58 | die("no cluster spec available") 59 | } 60 | 61 | newCd := cd.DeepCopy() 62 | keeperInfo := newCd.Keepers[keeperID] 63 | if keeperInfo == nil { 64 | die("keeper doesn't exist") 65 | } 66 | 67 | keeperDb := newCd.FindDB(keeperInfo) 68 | 69 | if keeperDb != nil { 70 | if newCd.Cluster.Status.Master == keeperDb.UID { 71 | die("keeper assigned db is the current cluster master db.") 72 | } 73 | } 74 | 75 | delete(newCd.Keepers, keeperID) 76 | if keeperDb != nil { 77 | delete(newCd.DBs, keeperDb.UID) 78 | } 79 | 80 | // NOTE: if the removed db is listed inside another db.Followers it'll will 81 | // be cleaned by the sentinels 82 | 83 | _, err = store.AtomicPutClusterData(context.TODO(), newCd, pair) 84 | if err != nil { 85 | die("cannot update cluster data: %v", err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/stolonctl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "fmt" 21 | "os" 22 | "strings" 23 | 24 | "github.com/sorintlab/stolon/cmd" 25 | "github.com/sorintlab/stolon/internal/cluster" 26 | "github.com/sorintlab/stolon/internal/flagutil" 27 | "github.com/sorintlab/stolon/internal/store" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | const ( 33 | maxRetries = 3 34 | ) 35 | 36 | var CmdStolonCtl = &cobra.Command{ 37 | Use: "stolonctl", 38 | Short: "stolon command line client", 39 | Version: cmd.Version, 40 | PersistentPreRun: func(c *cobra.Command, args []string) { 41 | if c.Name() != "stolonctl" && c.Name() != "version" { 42 | if err := cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { 43 | die(err.Error()) 44 | } 45 | } 46 | }, 47 | // just defined to make --version work 48 | Run: func(c *cobra.Command, args []string) { _ = c.Help() }, 49 | } 50 | 51 | type config struct { 52 | cmd.CommonConfig 53 | } 54 | 55 | var cfg config 56 | 57 | func init() { 58 | cfg.IsStolonCtl = true 59 | cmd.AddCommonFlags(CmdStolonCtl, &cfg.CommonConfig) 60 | } 61 | 62 | var cmdVersion = &cobra.Command{ 63 | Use: "version", 64 | Run: versionCommand, 65 | Short: "Display the version", 66 | } 67 | 68 | func init() { 69 | CmdStolonCtl.AddCommand(cmdVersion) 70 | } 71 | 72 | func versionCommand(c *cobra.Command, args []string) { 73 | stdout("stolonctl version %s", cmd.Version) 74 | } 75 | 76 | func Execute() { 77 | if err := flagutil.SetFlagsFromEnv(CmdStolonCtl.PersistentFlags(), "STOLONCTL"); err != nil { 78 | log.Fatal(err) 79 | } 80 | if err := CmdStolonCtl.Execute(); err != nil { 81 | log.Fatal(err) 82 | } 83 | } 84 | 85 | func stderr(format string, a ...interface{}) { 86 | out := fmt.Sprintf(format, a...) 87 | fmt.Fprintln(os.Stderr, strings.TrimSuffix(out, "\n")) 88 | } 89 | 90 | func stdout(format string, a ...interface{}) { 91 | out := fmt.Sprintf(format, a...) 92 | fmt.Fprintln(os.Stdout, strings.TrimSuffix(out, "\n")) 93 | } 94 | 95 | func die(format string, a ...interface{}) { 96 | stderr(format, a...) 97 | os.Exit(1) 98 | } 99 | 100 | func getClusterData(e store.Store) (*cluster.ClusterData, *store.KVPair, error) { 101 | cd, pair, err := e.GetClusterData(context.TODO()) 102 | if err != nil { 103 | return nil, nil, fmt.Errorf("cannot get cluster data: %v", err) 104 | } 105 | if cd == nil { 106 | return nil, nil, fmt.Errorf("nil cluster data: %v", err) 107 | } 108 | if cd.FormatVersion != cluster.CurrentCDFormatVersion { 109 | return nil, nil, fmt.Errorf("unsupported cluster data format version %d", cd.FormatVersion) 110 | } 111 | if err := cd.Cluster.Spec.Validate(); err != nil { 112 | return nil, nil, fmt.Errorf("clusterdata validation failed: %v", err) 113 | } 114 | return cd, pair, nil 115 | } 116 | 117 | func askConfirmation(message string) (bool, error) { 118 | in := bufio.NewReader(os.Stdin) 119 | for { 120 | fmt.Fprint(os.Stdout, message) 121 | input, err := in.ReadString('\n') 122 | if err != nil { 123 | return false, fmt.Errorf("error reading input: %v", err) 124 | } 125 | switch input { 126 | case "yes\n": 127 | return true, nil 128 | case "no\n": 129 | return false, nil 130 | default: 131 | stdout("Please enter 'yes' or 'no'") 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cmd/stolonctl/cmd/update.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 cmd 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | 24 | cmdcommon "github.com/sorintlab/stolon/cmd" 25 | "github.com/sorintlab/stolon/internal/cluster" 26 | "github.com/sorintlab/stolon/internal/store" 27 | 28 | "github.com/spf13/cobra" 29 | "k8s.io/apimachinery/pkg/util/strategicpatch" 30 | ) 31 | 32 | var cmdUpdate = &cobra.Command{ 33 | Use: "update", 34 | Run: update, 35 | Short: "Update a cluster specification", 36 | } 37 | 38 | type updateOptions struct { 39 | patch bool 40 | file string 41 | } 42 | 43 | var updateOpts updateOptions 44 | 45 | func init() { 46 | cmdUpdate.PersistentFlags().BoolVarP(&updateOpts.patch, "patch", "p", false, "patch the current cluster specification instead of replacing it") 47 | cmdUpdate.PersistentFlags().StringVarP(&updateOpts.file, "file", "f", "", "file containing a complete cluster specification or a patch to apply to the current cluster specification") 48 | 49 | CmdStolonCtl.AddCommand(cmdUpdate) 50 | } 51 | 52 | func patchClusterSpec(cs *cluster.ClusterSpec, p []byte) (*cluster.ClusterSpec, error) { 53 | csj, err := json.Marshal(cs) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to marshal cluster spec: %v", err) 56 | } 57 | 58 | newcsj, err := strategicpatch.StrategicMergePatch(csj, p, &cluster.ClusterSpec{}) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to merge patch cluster spec: %v", err) 61 | } 62 | var newcs *cluster.ClusterSpec 63 | if err := json.Unmarshal(newcsj, &newcs); err != nil { 64 | return nil, fmt.Errorf("failed to unmarshal patched cluster spec: %v", err) 65 | } 66 | return newcs, nil 67 | } 68 | 69 | func update(cmd *cobra.Command, args []string) { 70 | if len(args) > 1 { 71 | die("too many arguments") 72 | } 73 | if updateOpts.file == "" && len(args) < 1 { 74 | die("no cluster spec provided as argument and no file provided (--file/-f option)") 75 | } 76 | if updateOpts.file != "" && len(args) == 1 { 77 | die("only one of cluster spec provided as argument or file must provided (--file/-f option)") 78 | } 79 | 80 | var data []byte 81 | if len(args) == 1 { 82 | data = []byte(args[0]) 83 | } else { 84 | var err error 85 | if updateOpts.file == "-" { 86 | data, err = ioutil.ReadAll(os.Stdin) 87 | if err != nil { 88 | die("cannot read from stdin: %v", err) 89 | } 90 | } else { 91 | data, err = ioutil.ReadFile(updateOpts.file) 92 | if err != nil { 93 | die("cannot read file: %v", err) 94 | } 95 | } 96 | } 97 | 98 | e, err := cmdcommon.NewStore(&cfg.CommonConfig) 99 | if err != nil { 100 | die("%v", err) 101 | } 102 | 103 | retry := 0 104 | for retry < maxRetries { 105 | cd, pair, err := getClusterData(e) 106 | if err != nil { 107 | die("%v", err) 108 | } 109 | if cd.Cluster == nil { 110 | die("no cluster spec available") 111 | } 112 | if cd.Cluster.Spec == nil { 113 | die("no cluster spec available") 114 | } 115 | 116 | var newcs *cluster.ClusterSpec 117 | if updateOpts.patch { 118 | newcs, err = patchClusterSpec(cd.Cluster.Spec, data) 119 | if err != nil { 120 | die("failed to patch cluster spec: %v", err) 121 | } 122 | } else { 123 | if err = json.Unmarshal(data, &newcs); err != nil { 124 | die("failed to unmarshal cluster spec: %v", err) 125 | } 126 | } 127 | if err = cd.Cluster.UpdateSpec(newcs); err != nil { 128 | die("Cannot update cluster spec: %v", err) 129 | } 130 | 131 | // retry if cd has been modified between reading and writing 132 | _, err = e.AtomicPutClusterData(context.TODO(), cd, pair) 133 | if err != nil { 134 | if err == store.ErrKeyModified { 135 | retry++ 136 | continue 137 | } 138 | die("cannot update cluster data: %v", err) 139 | } 140 | break 141 | } 142 | if retry == maxRetries { 143 | die("failed to update cluster data after %d retries", maxRetries) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /cmd/stolonctl/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 | "github.com/sorintlab/stolon/cmd/stolonctl/cmd" 19 | ) 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sorint.lab 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 | // 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 cmd 16 | 17 | var Version = "No version defined at build time" 18 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ## Stolon Documentation 2 | 3 | We suggest that you first read the [Stolon Architecture and Requirements](architecture.md) to understand the primary concepts and avoid possible mistakes. 4 | 5 | * [Stolon Architecture and Requirements](architecture.md) 6 | * [Commands Invocation](commands_invocation.md) 7 | * [Cluster Specification](cluster_spec.md) 8 | * [Cluster Initialization](initialization.md) 9 | * [Setting instance parameters](postgres_parameters.md) 10 | * [Custom pg_hba.conf entries](custom_pg_hba_entries.md) 11 | * [Stolon Client](stolonctl.md) 12 | * Backup/Restore 13 | * [Point In Time Recovery](pitr.md) 14 | * [Point In Time Recovery with wal-e](pitr_wal-e.md) 15 | * [Point In Time Recovery with wal-g](pitr_wal-g.md) 16 | * [Standby Cluster](standbycluster.md) 17 | 18 | ### Misc topics 19 | 20 | * [Enabling pg_rewind](pg_rewind.md) 21 | * [Enabling synchronous replication](syncrepl.md) 22 | * [PostgreSQL SSL/TLS setup](ssl.md) 23 | * [Forcing a failover](forcefailover.md) 24 | * [Service Discovery](service_discovery.md) 25 | 26 | ### Recipes 27 | 28 | * [Manual switchover without transactions loss](manual_switchover.md) 29 | 30 | ### Examples 31 | 32 | * [Simple test cluster](simplecluster.md) 33 | * [Kubernetes](../examples/kubernetes/README.md) 34 | * [Two (or more) nodes setup](twonodes.md) 35 | 36 | ### [FAQ](faq.md) 37 | -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/architecture.png -------------------------------------------------------------------------------- /doc/architecture_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/architecture_small.png -------------------------------------------------------------------------------- /doc/commands/stolon-keeper.md: -------------------------------------------------------------------------------- 1 | ## stolon-keeper 2 | 3 | 4 | 5 | ### Synopsis 6 | 7 | 8 | 9 | ``` 10 | stolon-keeper [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --can-be-master prevent keeper from being elected as master (default true) 17 | --can-be-synchronous-replica prevent keeper from being chosen as synchronous replica (default true) 18 | --cluster-name string cluster name 19 | --data-dir string data directory 20 | --disable-data-dir-locking disable locking on data dir. Warning! It'll cause data corruptions if two keepers are concurrently running with the same data dir. 21 | -h, --help help for stolon-keeper 22 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 23 | --log-color enable color in log output (default if attached to a terminal) 24 | --log-level string debug, info (default), warn or error (default "info") 25 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 26 | --pg-advertise-address string postgresql instance address from outside. Use it to expose ip different than local ip with a NAT networking config 27 | --pg-advertise-port string postgresql instance port from outside. Use it to expose port different than local port with a PAT networking config 28 | --pg-bin-path string absolute path to postgresql binaries. If empty they will be searched in the current PATH 29 | --pg-listen-address string postgresql instance listening address, local address used for the postgres instance. For all network interface, you can set the value to '*'. 30 | --pg-port string postgresql instance listening port (default "5432") 31 | --pg-repl-auth-method string postgres replication user auth method. Default is md5. (default "md5") 32 | --pg-repl-password string postgres replication user password. Only one of --pg-repl-password or --pg-repl-passwordfile must be provided. Must be the same for all keepers. 33 | --pg-repl-passwordfile string postgres replication user password file. Only one of --pg-repl-password or --pg-repl-passwordfile must be provided. Must be the same for all keepers. 34 | --pg-repl-username string postgres replication user name. Required. It'll be created on db initialization. Must be the same for all keepers. 35 | --pg-su-auth-method string postgres superuser auth method. Default is md5. (default "md5") 36 | --pg-su-password string postgres superuser password. Only one of --pg-su-password or --pg-su-passwordfile must be provided. Must be the same for all keepers. 37 | --pg-su-passwordfile string postgres superuser password file. Only one of --pg-su-password or --pg-su-passwordfile must be provided. Must be the same for all keepers) 38 | --pg-su-username string postgres superuser user name. Used for keeper managed instance access and pg_rewind based synchronization. It'll be created on db initialization. Defaults to the name of the effective user running stolon-keeper. Must be the same for all keepers. 39 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 40 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 41 | --store-cert-file string certificate file for client identification to the store 42 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 43 | --store-key string private key file for client identification to the store 44 | --store-prefix string the store base prefix (default "stolon/cluster") 45 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 46 | --store-timeout duration store request timeout (default 5s) 47 | --uid string keeper uid (must be unique in the cluster and can contain only lower-case letters, numbers and the underscore character). If not provided a random uid will be generated. 48 | ``` 49 | 50 | ###### Auto generated by spf13/cobra on 24-Feb-2021 51 | -------------------------------------------------------------------------------- /doc/commands/stolon-proxy.md: -------------------------------------------------------------------------------- 1 | ## stolon-proxy 2 | 3 | 4 | 5 | ### Synopsis 6 | 7 | 8 | 9 | ``` 10 | stolon-proxy [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --cluster-name string cluster name 17 | -h, --help help for stolon-proxy 18 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 19 | --listen-address string proxy listening address (default "127.0.0.1") 20 | --log-color enable color in log output (default if attached to a terminal) 21 | --log-level string debug, info (default), warn or error (default "info") 22 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 23 | --port string proxy listening port (default "5432") 24 | --stop-listening stop listening on store error (default true) 25 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 26 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 27 | --store-cert-file string certificate file for client identification to the store 28 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 29 | --store-key string private key file for client identification to the store 30 | --store-prefix string the store base prefix (default "stolon/cluster") 31 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 32 | --store-timeout duration store request timeout (default 5s) 33 | --tcp-keepalive-count int set tcp keepalive probe count number 34 | --tcp-keepalive-idle int set tcp keepalive idle (seconds) 35 | --tcp-keepalive-interval int set tcp keepalive interval (seconds) 36 | ``` 37 | 38 | ###### Auto generated by spf13/cobra on 24-Feb-2021 39 | -------------------------------------------------------------------------------- /doc/commands/stolon-sentinel.md: -------------------------------------------------------------------------------- 1 | ## stolon-sentinel 2 | 3 | 4 | 5 | ### Synopsis 6 | 7 | 8 | 9 | ``` 10 | stolon-sentinel [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --cluster-name string cluster name 17 | -h, --help help for stolon-sentinel 18 | --initial-cluster-spec string a file providing the initial cluster specification, used only at cluster initialization, ignored if cluster is already initialized 19 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 20 | --log-color enable color in log output (default if attached to a terminal) 21 | --log-level string debug, info (default), warn or error (default "info") 22 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 23 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 24 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 25 | --store-cert-file string certificate file for client identification to the store 26 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 27 | --store-key string private key file for client identification to the store 28 | --store-prefix string the store base prefix (default "stolon/cluster") 29 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 30 | --store-timeout duration store request timeout (default 5s) 31 | ``` 32 | 33 | ###### Auto generated by spf13/cobra on 24-Feb-2021 34 | -------------------------------------------------------------------------------- /doc/commands/stolonctl.md: -------------------------------------------------------------------------------- 1 | ## stolonctl 2 | 3 | stolon command line client 4 | 5 | ### Synopsis 6 | 7 | stolon command line client 8 | 9 | ``` 10 | stolonctl [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --cluster-name string cluster name 17 | -h, --help help for stolonctl 18 | --kube-context string name of the kubeconfig context to use 19 | --kube-namespace string name of the kubernetes namespace to use 20 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 21 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 22 | --log-level string debug, info (default), warn or error (default "info") 23 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 24 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 25 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 26 | --store-cert-file string certificate file for client identification to the store 27 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 28 | --store-key string private key file for client identification to the store 29 | --store-prefix string the store base prefix (default "stolon/cluster") 30 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 31 | --store-timeout duration store request timeout (default 5s) 32 | ``` 33 | 34 | ### SEE ALSO 35 | 36 | * [stolonctl clusterdata](stolonctl_clusterdata.md) - Manage current cluster data 37 | * [stolonctl failkeeper](stolonctl_failkeeper.md) - Force keeper as "temporarily" failed. The sentinel will compute a new clusterdata considering it as failed and then restore its state to the real one. 38 | * [stolonctl init](stolonctl_init.md) - Initialize a new cluster 39 | * [stolonctl promote](stolonctl_promote.md) - Promotes a standby cluster to a primary cluster 40 | * [stolonctl register](stolonctl_register.md) - Register stolon keepers for service discovery 41 | * [stolonctl removekeeper](stolonctl_removekeeper.md) - Removes keeper from cluster data 42 | * [stolonctl spec](stolonctl_spec.md) - Retrieve the current cluster specification 43 | * [stolonctl status](stolonctl_status.md) - Display the current cluster status 44 | * [stolonctl update](stolonctl_update.md) - Update a cluster specification 45 | * [stolonctl version](stolonctl_version.md) - Display the version 46 | 47 | ###### Auto generated by spf13/cobra on 24-Feb-2021 48 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_clusterdata.md: -------------------------------------------------------------------------------- 1 | ## stolonctl clusterdata 2 | 3 | Manage current cluster data 4 | 5 | ### Synopsis 6 | 7 | Manage current cluster data 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for clusterdata 13 | ``` 14 | 15 | ### Options inherited from parent commands 16 | 17 | ``` 18 | --cluster-name string cluster name 19 | --kube-context string name of the kubeconfig context to use 20 | --kube-namespace string name of the kubernetes namespace to use 21 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 22 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 23 | --log-level string debug, info (default), warn or error (default "info") 24 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 25 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 26 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 27 | --store-cert-file string certificate file for client identification to the store 28 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 29 | --store-key string private key file for client identification to the store 30 | --store-prefix string the store base prefix (default "stolon/cluster") 31 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 32 | --store-timeout duration store request timeout (default 5s) 33 | ``` 34 | 35 | ### SEE ALSO 36 | 37 | * [stolonctl](stolonctl.md) - stolon command line client 38 | * [stolonctl clusterdata read](stolonctl_clusterdata_read.md) - Retrieve the current cluster data 39 | * [stolonctl clusterdata write](stolonctl_clusterdata_write.md) - Write cluster data 40 | 41 | ###### Auto generated by spf13/cobra on 24-Feb-2021 42 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_clusterdata_read.md: -------------------------------------------------------------------------------- 1 | ## stolonctl clusterdata read 2 | 3 | Retrieve the current cluster data 4 | 5 | ### Synopsis 6 | 7 | Retrieve the current cluster data 8 | 9 | ``` 10 | stolonctl clusterdata read [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for read 17 | --pretty pretty print 18 | ``` 19 | 20 | ### Options inherited from parent commands 21 | 22 | ``` 23 | --cluster-name string cluster name 24 | --kube-context string name of the kubeconfig context to use 25 | --kube-namespace string name of the kubernetes namespace to use 26 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 27 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 28 | --log-level string debug, info (default), warn or error (default "info") 29 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 30 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 31 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 32 | --store-cert-file string certificate file for client identification to the store 33 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 34 | --store-key string private key file for client identification to the store 35 | --store-prefix string the store base prefix (default "stolon/cluster") 36 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 37 | --store-timeout duration store request timeout (default 5s) 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [stolonctl clusterdata](stolonctl_clusterdata.md) - Manage current cluster data 43 | 44 | ###### Auto generated by spf13/cobra on 24-Feb-2021 45 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_clusterdata_write.md: -------------------------------------------------------------------------------- 1 | ## stolonctl clusterdata write 2 | 3 | Write cluster data 4 | 5 | ### Synopsis 6 | 7 | Write cluster data 8 | 9 | ``` 10 | stolonctl clusterdata write [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -f, --file string file containing the new cluster data 17 | -h, --help help for write 18 | -y, --yes don't ask for confirmation 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --cluster-name string cluster name 25 | --kube-context string name of the kubeconfig context to use 26 | --kube-namespace string name of the kubernetes namespace to use 27 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 28 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 29 | --log-level string debug, info (default), warn or error (default "info") 30 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 31 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 32 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 33 | --store-cert-file string certificate file for client identification to the store 34 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 35 | --store-key string private key file for client identification to the store 36 | --store-prefix string the store base prefix (default "stolon/cluster") 37 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 38 | --store-timeout duration store request timeout (default 5s) 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [stolonctl clusterdata](stolonctl_clusterdata.md) - Manage current cluster data 44 | 45 | ###### Auto generated by spf13/cobra on 24-Feb-2021 46 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_failkeeper.md: -------------------------------------------------------------------------------- 1 | ## stolonctl failkeeper 2 | 3 | Force keeper as "temporarily" failed. The sentinel will compute a new clusterdata considering it as failed and then restore its state to the real one. 4 | 5 | ### Synopsis 6 | 7 | Force keeper as "temporarily" failed. It's just a one shot operation, the sentinel will compute a new clusterdata considering the keeper as failed and then restore its state to the real one. For example, if the force failed keeper is a master, the sentinel will try to elect a new master. If no new master can be elected, the force failed keeper, if really healthy, will be re-elected as master 8 | 9 | ``` 10 | stolonctl failkeeper [keeper uid] [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for failkeeper 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | --cluster-name string cluster name 23 | --kube-context string name of the kubeconfig context to use 24 | --kube-namespace string name of the kubernetes namespace to use 25 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 26 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 27 | --log-level string debug, info (default), warn or error (default "info") 28 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 29 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 30 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 31 | --store-cert-file string certificate file for client identification to the store 32 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 33 | --store-key string private key file for client identification to the store 34 | --store-prefix string the store base prefix (default "stolon/cluster") 35 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 36 | --store-timeout duration store request timeout (default 5s) 37 | ``` 38 | 39 | ### SEE ALSO 40 | 41 | * [stolonctl](stolonctl.md) - stolon command line client 42 | 43 | ###### Auto generated by spf13/cobra on 24-Feb-2021 44 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_init.md: -------------------------------------------------------------------------------- 1 | ## stolonctl init 2 | 3 | Initialize a new cluster 4 | 5 | ### Synopsis 6 | 7 | Initialize a new cluster 8 | 9 | ``` 10 | stolonctl init [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -f, --file string file contaning the new cluster spec 17 | -h, --help help for init 18 | -y, --yes don't ask for confirmation 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --cluster-name string cluster name 25 | --kube-context string name of the kubeconfig context to use 26 | --kube-namespace string name of the kubernetes namespace to use 27 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 28 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 29 | --log-level string debug, info (default), warn or error (default "info") 30 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 31 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 32 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 33 | --store-cert-file string certificate file for client identification to the store 34 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 35 | --store-key string private key file for client identification to the store 36 | --store-prefix string the store base prefix (default "stolon/cluster") 37 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 38 | --store-timeout duration store request timeout (default 5s) 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [stolonctl](stolonctl.md) - stolon command line client 44 | 45 | ###### Auto generated by spf13/cobra on 24-Feb-2021 46 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_promote.md: -------------------------------------------------------------------------------- 1 | ## stolonctl promote 2 | 3 | Promotes a standby cluster to a primary cluster 4 | 5 | ### Synopsis 6 | 7 | Promotes a standby cluster to a primary cluster 8 | 9 | ``` 10 | stolonctl promote [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for promote 17 | -y, --yes don't ask for confirmation 18 | ``` 19 | 20 | ### Options inherited from parent commands 21 | 22 | ``` 23 | --cluster-name string cluster name 24 | --kube-context string name of the kubeconfig context to use 25 | --kube-namespace string name of the kubernetes namespace to use 26 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 27 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 28 | --log-level string debug, info (default), warn or error (default "info") 29 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 30 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 31 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 32 | --store-cert-file string certificate file for client identification to the store 33 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 34 | --store-key string private key file for client identification to the store 35 | --store-prefix string the store base prefix (default "stolon/cluster") 36 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 37 | --store-timeout duration store request timeout (default 5s) 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [stolonctl](stolonctl.md) - stolon command line client 43 | 44 | ###### Auto generated by spf13/cobra on 24-Feb-2021 45 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_register.md: -------------------------------------------------------------------------------- 1 | ## stolonctl register 2 | 3 | Register stolon keepers for service discovery 4 | 5 | ### Synopsis 6 | 7 | Register stolon keepers for service discovery 8 | 9 | ``` 10 | stolonctl register [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --debug enable debug logging 17 | -h, --help help for register 18 | --register-backend string register backend type (consul) (default "consul") 19 | --register-ca-file string verify certificates of HTTPS-enabled register servers using this CA bundle 20 | --register-cert-file string certificate file for client identification to the register 21 | --register-endpoints string a comma-delimited list of register endpoints (use https scheme for tls communication) defaults: http://127.0.0.1:8500 for consul (default "http://127.0.0.1:8500") 22 | --register-key string private key file for client identification to the register 23 | --register-master register master as well for service discovery (use it with caution!!!) 24 | --register-skip-tls-verify skip register certificate verification (insecure!!!) 25 | --sleep-interval int number of seconds to sleep before probing for change (default 10) 26 | --tag-master-as string a comma-delimited list of tag to be used when registering master (default "master") 27 | --tag-slave-as string a comma-delimited list of tag to be used when registering slave (default "slave") 28 | ``` 29 | 30 | ### Options inherited from parent commands 31 | 32 | ``` 33 | --cluster-name string cluster name 34 | --kube-context string name of the kubeconfig context to use 35 | --kube-namespace string name of the kubernetes namespace to use 36 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 37 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 38 | --log-level string debug, info (default), warn or error (default "info") 39 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 40 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 41 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 42 | --store-cert-file string certificate file for client identification to the store 43 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 44 | --store-key string private key file for client identification to the store 45 | --store-prefix string the store base prefix (default "stolon/cluster") 46 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 47 | --store-timeout duration store request timeout (default 5s) 48 | ``` 49 | 50 | ### SEE ALSO 51 | 52 | * [stolonctl](stolonctl.md) - stolon command line client 53 | 54 | ###### Auto generated by spf13/cobra on 24-Feb-2021 55 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_removekeeper.md: -------------------------------------------------------------------------------- 1 | ## stolonctl removekeeper 2 | 3 | Removes keeper from cluster data 4 | 5 | ### Synopsis 6 | 7 | Removes keeper from cluster data 8 | 9 | ``` 10 | stolonctl removekeeper [keeper uid] [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for removekeeper 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | --cluster-name string cluster name 23 | --kube-context string name of the kubeconfig context to use 24 | --kube-namespace string name of the kubernetes namespace to use 25 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 26 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 27 | --log-level string debug, info (default), warn or error (default "info") 28 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 29 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 30 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 31 | --store-cert-file string certificate file for client identification to the store 32 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 33 | --store-key string private key file for client identification to the store 34 | --store-prefix string the store base prefix (default "stolon/cluster") 35 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 36 | --store-timeout duration store request timeout (default 5s) 37 | ``` 38 | 39 | ### SEE ALSO 40 | 41 | * [stolonctl](stolonctl.md) - stolon command line client 42 | 43 | ###### Auto generated by spf13/cobra on 24-Feb-2021 44 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_spec.md: -------------------------------------------------------------------------------- 1 | ## stolonctl spec 2 | 3 | Retrieve the current cluster specification 4 | 5 | ### Synopsis 6 | 7 | Retrieve the current cluster specification 8 | 9 | ``` 10 | stolonctl spec [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | --defaults also show default values 17 | -h, --help help for spec 18 | ``` 19 | 20 | ### Options inherited from parent commands 21 | 22 | ``` 23 | --cluster-name string cluster name 24 | --kube-context string name of the kubeconfig context to use 25 | --kube-namespace string name of the kubernetes namespace to use 26 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 27 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 28 | --log-level string debug, info (default), warn or error (default "info") 29 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 30 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 31 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 32 | --store-cert-file string certificate file for client identification to the store 33 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 34 | --store-key string private key file for client identification to the store 35 | --store-prefix string the store base prefix (default "stolon/cluster") 36 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 37 | --store-timeout duration store request timeout (default 5s) 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [stolonctl](stolonctl.md) - stolon command line client 43 | 44 | ###### Auto generated by spf13/cobra on 24-Feb-2021 45 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_status.md: -------------------------------------------------------------------------------- 1 | ## stolonctl status 2 | 3 | Display the current cluster status 4 | 5 | ### Synopsis 6 | 7 | Display the current cluster status 8 | 9 | ``` 10 | stolonctl status [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -f, --format string output format 17 | -h, --help help for status 18 | ``` 19 | 20 | ### Options inherited from parent commands 21 | 22 | ``` 23 | --cluster-name string cluster name 24 | --kube-context string name of the kubeconfig context to use 25 | --kube-namespace string name of the kubernetes namespace to use 26 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 27 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 28 | --log-level string debug, info (default), warn or error (default "info") 29 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 30 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 31 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 32 | --store-cert-file string certificate file for client identification to the store 33 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 34 | --store-key string private key file for client identification to the store 35 | --store-prefix string the store base prefix (default "stolon/cluster") 36 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 37 | --store-timeout duration store request timeout (default 5s) 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [stolonctl](stolonctl.md) - stolon command line client 43 | 44 | ###### Auto generated by spf13/cobra on 24-Feb-2021 45 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_update.md: -------------------------------------------------------------------------------- 1 | ## stolonctl update 2 | 3 | Update a cluster specification 4 | 5 | ### Synopsis 6 | 7 | Update a cluster specification 8 | 9 | ``` 10 | stolonctl update [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -f, --file string file containing a complete cluster specification or a patch to apply to the current cluster specification 17 | -h, --help help for update 18 | -p, --patch patch the current cluster specification instead of replacing it 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --cluster-name string cluster name 25 | --kube-context string name of the kubeconfig context to use 26 | --kube-namespace string name of the kubernetes namespace to use 27 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 28 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 29 | --log-level string debug, info (default), warn or error (default "info") 30 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 31 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 32 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 33 | --store-cert-file string certificate file for client identification to the store 34 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 35 | --store-key string private key file for client identification to the store 36 | --store-prefix string the store base prefix (default "stolon/cluster") 37 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 38 | --store-timeout duration store request timeout (default 5s) 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [stolonctl](stolonctl.md) - stolon command line client 44 | 45 | ###### Auto generated by spf13/cobra on 24-Feb-2021 46 | -------------------------------------------------------------------------------- /doc/commands/stolonctl_version.md: -------------------------------------------------------------------------------- 1 | ## stolonctl version 2 | 3 | Display the version 4 | 5 | ### Synopsis 6 | 7 | Display the version 8 | 9 | ``` 10 | stolonctl version [flags] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for version 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | --cluster-name string cluster name 23 | --kube-context string name of the kubeconfig context to use 24 | --kube-namespace string name of the kubernetes namespace to use 25 | --kube-resource-kind string the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported) 26 | --kubeconfig string path to kubeconfig file. Overrides $KUBECONFIG 27 | --log-level string debug, info (default), warn or error (default "info") 28 | --metrics-listen-address string metrics listen address i.e "0.0.0.0:8080" (disabled by default) 29 | --store-backend string store backend type (etcdv2/etcd, etcdv3, consul or kubernetes) 30 | --store-ca-file string verify certificates of HTTPS-enabled store servers using this CA bundle 31 | --store-cert-file string certificate file for client identification to the store 32 | --store-endpoints string a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul) 33 | --store-key string private key file for client identification to the store 34 | --store-prefix string the store base prefix (default "stolon/cluster") 35 | --store-skip-tls-verify skip store certificate verification (insecure!!!) 36 | --store-timeout duration store request timeout (default 5s) 37 | ``` 38 | 39 | ### SEE ALSO 40 | 41 | * [stolonctl](stolonctl.md) - stolon command line client 42 | 43 | ###### Auto generated by spf13/cobra on 24-Feb-2021 44 | -------------------------------------------------------------------------------- /doc/commands_invocation.md: -------------------------------------------------------------------------------- 1 | ## Commands invocation 2 | 3 | There are 4 commands provided by stolon: 4 | 5 | * [stolon-keeper](commands/stolon-keeper.md) 6 | * [stolon-sentinel](commands/stolon-sentinel.md) 7 | * [stolon-proxy](commands/stolon-proxy.md) 8 | * [stolonctl](commands/stolonctl.md) 9 | 10 | every command has different options and when called without any option or with `--help` they'll show an help with a description for every subcommand and option. Please take also a look at the various examples to see how the commands' options are used. 11 | 12 | An option can be specified in the command line or as an environment variables. 13 | 14 | Environment variables are prefixed with a string related to the command and take the name of the options but are UPPERCASE and any dashes are replaced by underscores. 15 | 16 | For example for the `stolon-keeper` `--data-dir` command line option the equivalent environment variable is `STKEEPER_DATA_DIR` 17 | 18 | 19 | | Command | Environment variable prefix | 20 | |--------------------|-----------------------------| 21 | | stolon-keeper | STKEEPER | 22 | | stolon-sentinel | STSENTINEL | 23 | | stolon-proxy | STPROXY | 24 | | stolonctl | STOLONCTL | 25 | -------------------------------------------------------------------------------- /doc/custom_pg_hba_entries.md: -------------------------------------------------------------------------------- 1 | ## Setting custom pg_hba.conf entries 2 | 3 | Stolon manages the pg_hba.conf file entries. The first rules are generated by stolon to permit local keeper connections and remote replication connections since these are needed to ensure the correct operation of the cluster. 4 | 5 | Users can specify custom pg_hba.conf entries setting the [cluster_specification](cluster_spec.md) `pgHBA` option. It must be a list of string containing additional pg_hba.conf entries. They will be added to the pg_hba.conf generated by stolon. 6 | 7 | Since clients connection will pass through the stolon-proxy the host part of the entries should match at least the stolon-proxies source addresses. For the same reason it's not possible to directly filter by client. If you have clients that requires different accesses you should use different set of stolon proxies for every kind of access. 8 | 9 | **NOTE**: these lines aren't validated so if some of them are wrong postgres will refuse to start or, on reload, will log a warning and ignore the updated pg_hba.conf file. Stolon will just check that the string doesn't contain newlines characters. 10 | 11 | By default, if no custom pg_hba entries are defined (clusterpsec pgHBA option is null, not an empty list), to keep backward compatibility, stolon will add two rules to permit tcp (both ipv4 and ipv6) connections from every host to all dbs and usernames with md5 password authentication: 12 | 13 | ``` 14 | host all all 0.0.0.0/0 md5 15 | host all all ::0/0 md5 16 | ``` 17 | -------------------------------------------------------------------------------- /doc/forcefailover.md: -------------------------------------------------------------------------------- 1 | ## Forcing a failover 2 | 3 | You can force a "master" keeper failover using the [stolonctl failkeeper](commands/stolonctl_failkeeper.md) 4 | 5 | This commands forces a keeper as "temporarily" failed. It's just a one shot operation, the sentinel will compute a new clusterdata considering the keeper as failed and then restore its state to the real one. 6 | 7 | For example, if the force failed keeper is a master, the sentinel will try to elect a new master. If no new master can be elected, the force failed keeper, if really healthy, will be re-elected as master 8 | 9 | To avoid losing any transaction when using asynchronous replication take a look at this recipe: 10 | 11 | * [Manual switchover without transactions loss](manual_switchover.md) 12 | -------------------------------------------------------------------------------- /doc/initialization.md: -------------------------------------------------------------------------------- 1 | ## Cluster initialization 2 | 3 | The cluster initialization can be done at the first time when the cluster isn't initialized (empty cluster data in the store) or also on an already initialized cluster, dropping it and creating a new one. In this case be careful that the current cluster data will be overwritten and, depending on how you are specifying the cluster specification, the keeper may erase/overwrite their managed postgreg db cluster. 4 | 5 | ### Initialize a new stolon cluster with a new postgres db cluster 6 | 7 | You can initialize new stolon cluster with a new postgres db cluster using 8 | ``` 9 | stolonctl init 10 | ``` 11 | 12 | This is the same as passing a cluster specification with `initMode` set to `new`: 13 | 14 | ``` 15 | stolonctl init '{ "initMode": "new" }' 16 | ``` 17 | 18 | The postgres parameters generated by the `initdb` command will be merged back inside the cluster specification `pgParameters` map. See the related [postgres parameters](postgres_parameters.md) documentation. 19 | 20 | ### Initialize a new stolon cluster using an existing keeper 21 | 22 | This can be useful in different cases: 23 | 24 | * To reinitialize your store (for example if you have permanently lost the store). 25 | * if you want, for whatever reason, to force a new master (with all the possible problems caused by doing this). 26 | 27 | Given the declarative nature of the cluster specification you cannot force a new master. So, if you have an existing keeper that you want to set as the new master, you have to initialize a new cluster asking that it should be initialized with a specified keeper as the initial master: 28 | 29 | 30 | ``` 31 | stolonctl init '{ "initMode": "existing", "existingConfig": { "keeperUID": "keeper01" } }' 32 | ``` 33 | 34 | The existing instance postgres parameters will be merged back inside the cluster specification `pgParameters` map. See the related [postgres parameters](postgres_parameters.md) documentation. 35 | 36 | ### First time initialization without stolonctl 37 | 38 | You can also provide the `--initial-cluster-spec` option to the `stolon-sentinel` but this will work only when the clusterdata in the store is empty. 39 | -------------------------------------------------------------------------------- /doc/manual_switchover.md: -------------------------------------------------------------------------------- 1 | # Manual switch master without losing any transaction 2 | 3 | If for any reason (eg. maintenance) you want to switch the current master to another one without losing any transaction you can do this in these ways: 4 | 5 | * If you've synchronous replication enabled you can just stop/[forcefailover](forcefailover.md) the current master keeper, one of the synchronous standbys will be elected as the new master. 6 | 7 | * If you aren't using synchronous replication you can just temporarily enable it (see [here](syncrepl.md)), wait that the cluster reconfigures some synchronous standbys (you can monitor `pg_stat_replication` for a standby with `sync_state` = `sync`) and then stop/[forcefailover](forcefailover.md) the master keeper, wait for a new synchronous standby to be elected and disable synchronous replication. 8 | 9 | -------------------------------------------------------------------------------- /doc/pg_rewind.md: -------------------------------------------------------------------------------- 1 | # pg_rewind 2 | 3 | Stolon can use [pg_rewind](http://www.postgresql.org/docs/current/static/app-pgrewind.html) to speedup instance resynchronization (for example resyncing an old master or a slave ahead of the current master) without the need to copy all the new master data. 4 | 5 | ## Enabling 6 | 7 | It can be enabled setting to true the cluster specification option `usePgrewind` (defaults to false): 8 | 9 | ``` bash 10 | stolonctl [cluster options] update --patch '{ "usePgrewind" : true }' 11 | ``` 12 | 13 | This will also enable the `wal_log_hints` postgresql parameter. If previously `wal_log_hints` wasn't enabled you should restart the postgresql instances (you can do so restarting the `stolon-keeper`) 14 | 15 | pg_rewind needs to connect to the master database with a superuser role (see the [Stolon Architecture and Requirements](architecture.md)). 16 | -------------------------------------------------------------------------------- /doc/pitr.md: -------------------------------------------------------------------------------- 1 | ## Point in time recovery 2 | 3 | Stolon can do a point a time recovery starting from an existing backup. 4 | 5 | * [This](pitr_wal-g.md) example shows how to do point in time recovery with stolon using [wal-g](https://github.com/wal-g/wal-g) 6 | * [This](pitr_wal-e.md) example shows how to do point in time recovery with stolon using [wal-e](https://github.com/wal-e/wal-e) 7 | 8 | 9 | ### Backups 10 | 11 | #### Base backups 12 | 13 | stolon doesn't trigger base backups, you can run them at your preferred times and with your preferred scheduler. 14 | 15 | #### Archive backups 16 | 17 | With stolon you should instead enable `archive_mode` and set the `archive_command`, there's nothing different than a typical [postgresql backup](https://www.postgresql.org/docs/current/static/continuous-archiving.html). 18 | 19 | ``` 20 | stolonctl update --patch '{ "pgParameters" : { "archive_mode": "on", "archive_command": "/path/to/your/archive/command %p" } }' 21 | ``` 22 | 23 | `archive_mode` and the related `archive_command` will be enabled for all the instances (master and standbys). This is done to avoid losing some wals to backup when the current master keeper is down and a new master is elected. We suggest to define your archive command script to avoid backing up the same wal from all the instances (for example doing this only when the instance is the stolon master and just removing the wal when the instance is a stolon standby). 24 | 25 | ### Execute a point in time recovery 26 | 27 | To trigger a point in time recovery the cluster has to be (re)initialized using the `pitr` initMode in the cluster specification and setting in the `pitrConfig` the `dataRestoreCommand` and the archive `restoreCommand`. 28 | 29 | The `dataRestoreCommand` should containt the local shell command to execute to restore the full backup. Any `%d` in the string is replaced by the full path to the data directory. The command should return a 0 exit status if (and only if) it succeeds. 30 | 31 | The archive `restoreCommand` will be used as is in the generated `recovery.conf` file. For its value see the related [postgresql doc](https://www.postgresql.org/docs/current/static/archive-recovery-settings.html) 32 | 33 | ``` 34 | stolonctl init '{ "initMode": "pitr", "pitrConfig": { "dataRestoreCommand": "/path/to/your/backup/restore/command \"%d\"" , "archiveRecoverySettings": { "restoreCommand": "/path/to/your/archive/restore/command \"%f\" \"%p\"" } } }' 35 | ``` 36 | 37 | Note: the `\"` is needed by json to put double quotes inside strings. We aren't using single quotes since they are just used to pass to the shell the full json as a single argument. 38 | 39 | When initializing a cluster in pitr init mode a random registered keeper will be choosed and it'll start restoring the database with these steps: 40 | 41 | * Remove the current data directory 42 | * Call the `dataRestoreCommand` expanding every %d to the data directory full path. If it exits with a non zero exit code then stop here since something went wrong. 43 | * Create a `recovery.conf` with the right parameters and with `restore_command` set to `restoreCommand`. 44 | * Start the postgres instance and wait for the archive recovery. 45 | 46 | 47 | If something goes wrong you can see the errors in the keeper's logs or in postgresql log (if these are related to the archive restore step) and you can retrigger a new pitr reinitializing the cluster. 48 | -------------------------------------------------------------------------------- /doc/pitr_wal-e.md: -------------------------------------------------------------------------------- 1 | ## Point in time recovery with wal-e 2 | 3 | This example shows how to do point in time recovery with stolon using [wal-e](https://github.com/wal-e/wal-e) 4 | 5 | [wal-e](https://github.com/wal-e/wal-e) correctly suggests to not put environment variables containing secret data (like aws secret keys) inside the `archive_command` since every user connected to postgres could read them. In its examples wal-e suggests to use the `envdir` command to set the wal-e required environment variables or (since some distribution don't have it) just use a custom script that sets them. 6 | 7 | ### Backups 8 | 9 | #### Base backups 10 | 11 | Take the base backups using the `wal-e backup-push` command. 12 | 13 | #### Archive backups 14 | 15 | For doing this you should set at least the `archive_mode` and the `archive_command` pgParameters in the cluster spec. Wal-e will be used as the archive command: 16 | 17 | ``` 18 | stolonctl update --patch '{ "pgParameters" : { "archive_mode": "on", "archive_command": "envdir /etc/wal-e.d/env wal-e wal-push %p" } }' 19 | ``` 20 | 21 | 22 | ### Execute a point in time recovery 23 | 24 | Note: looks like wal-e doesn't backups various config files like `postgresql.conf`, `pg_hba.conf`. While `pg_hba.conf` is currently generated by stolon, you'd like to keep the previous postgres parameters after the restore. For doing this there're two different ways: 25 | 26 | * if you want to backup the `postgresql.conf` you should do this outside `wal-e`. To restore it you have to create a `dataRestoreCommand` that will restore it after the `wal-e backup fetch` command. 27 | * if you don't want to backup/restore it than you can just set all the `pgParameters` inside the [cluster specification](cluster_spec.md) 28 | 29 | ``` 30 | stolonctl init '{ "initMode": "pitr", "pitrConfig": { "dataRestoreCommand": "envdir /etc/wal-e.d/env wal-e backup-fetch %d LATEST" , "archiveRecoverySettings": { "restoreCommand": "envdir /etc/wal-e.d/env wal-e wal-fetch \"%f\" \"%p\"" } } }' 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /doc/pitr_wal-g.md: -------------------------------------------------------------------------------- 1 | ## Point in time recovery with wal-g 2 | 3 | [wal-g](https://github.com/wal-g/wal-g) is the successor of wal-e, which no longer seems to be in active development! 4 | This example shows how to do point in time recovery with stolon using [wal-g](https://github.com/wal-g/wal-g) 5 | 6 | [wal-g](https://github.com/wal-g/wal-g) correctly suggests to not put environment variables containing secret data (like aws secret keys) inside the `archive_command` since every user connected to postgres could read them. In its examples wal-g suggests to use the `envdir` command to set the wal-g required environment variables or (since some distribution don't have it) just use a custom script that sets them. 7 | 8 | ### Backups 9 | 10 | #### Base backups 11 | 12 | Take the base backups using the `wal-g backup-push` command. 13 | 14 | #### Archive backups 15 | 16 | For doing this you should set at least the `archive_mode` and the `archive_command` pgParameters in the cluster spec. Wal-g will be used as the archive command: 17 | 18 | ``` 19 | stolonctl update --patch '{ "pgParameters" : { "archive_mode": "on", "archive_command": "envdir /etc/wal-g.d/env wal-g wal-push %p" } }' 20 | ``` 21 | 22 | 23 | ### Execute a point in time recovery 24 | 25 | Note: looks like wal-g doesn't backups various config files like `postgresql.conf`, `pg_hba.conf`. While `pg_hba.conf` is currently generated by stolon, you'd like to keep the previous postgres parameters after the restore. For doing this there're two different ways: 26 | 27 | * if you want to backup the `postgresql.conf` you should do this outside `wal-g`. To restore it you have to create a `dataRestoreCommand` that will restore it after the `wal-g backup fetch` command. 28 | * if you don't want to backup/restore it than you can just set all the `pgParameters` inside the [cluster specification](cluster_spec.md) 29 | 30 | ``` 31 | stolonctl init '{ "initMode": "pitr", "pitrConfig": { "dataRestoreCommand": "envdir /etc/wal-g.d/env wal-g backup-fetch %d LATEST" , "archiveRecoverySettings": { "restoreCommand": "envdir /etc/wal-g.d/env wal-g wal-fetch \"%f\" \"%p\"" } } }' 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /doc/postgres_parameters.md: -------------------------------------------------------------------------------- 1 | ## Setting PostgreSQL server parameters 2 | 3 | The unique central point for managing the postgresql parameters is the [cluster_specification](cluster_spec.md) `pgParameters` map. This makes easy to centrally manage and keep in sync the postgresql paramaters of all the db instances in the cluster. The keepers will generate a `postgresql.conf` that contains the parameters defined in the [cluster_specification](cluster_spec.md) `pgParameters` map. 4 | 5 | The user can change the parameters at every time and the keepers will update the `postgresql.conf` and reload the instance. 6 | 7 | If some parameters needs an instance restart to be applied this should be manually done by the user restarting the keepers. 8 | 9 | ### Ignored parameters 10 | 11 | These parameters, if defined in the cluster specification, will be ignored since they are managed by stolon and cannot be defined by the user: 12 | 13 | ``` 14 | listen_addresses 15 | port 16 | unix_socket_directories 17 | wal_keep_segments 18 | wal_log_hints 19 | hot_standby 20 | max_replication_slots 21 | max_wal_senders 22 | synchronous_standby_names 23 | ``` 24 | 25 | ## Special cases 26 | 27 | ### wal_level 28 | 29 | since stolon requires a `wal_level` value of at least `replica` (or `hot_standby` for pg < 9.6) if you leave it unspecificed in the `pgParameters` or if you specify a wrong `wal_level` or a value lesser than `replica` or `hot_standby` (like `minimal`) it'll be overridden by the minimal working value (`replica` or `hot_standby`). 30 | 31 | i.e. if you want to also save logical replication information in the wal files you can specify a `wal_level` set to `logical`. 32 | 33 | ## Parameters validity checks 34 | 35 | Actually stolon doesn't do any check on the provided configurations, so, if the provided parameters are wrong this won't create problems at instance reload (just some warning in the postgresql logs) but at the next instance restart, it'll probably fail making the instance not available (thus triggering failover if it's the master or other changes in the clusterview). 36 | 37 | ## Initialization parameters 38 | 39 | When [initializing the cluster](initialization.md), by default, stolon will merge in the cluster spec the parameters that the instance had at the end of the initialization, practically: 40 | 41 | * When initMode is new, it'll merge the initdb generated parameters. 42 | * When initMode is existing it'll merge the parameters of the existing instance. 43 | 44 | To disable this behavior just set `mergePgParameters` to false in the cluster spec and manually set all the pgParameters in the cluster specification. 45 | 46 | ## postgresql.auto.conf and ALTER SYSTEM commands 47 | 48 | Since postgresql.auto.conf overrides postgresql.conf parameters, changing some of them with ALTER SYSTEM could break the cluster (parameters managed by stolon could be overridden) and make pg parameters different between the instances. 49 | 50 | To avoid this stolon disables the execution of ALTER SYSTEM commands making postgresql.auto.conf a symlink to /dev/null. When an ALTER SYSTEM command is executed it'll return an error. 51 | 52 | ## Restart postgres on changing some pg parameters 53 | 54 | There are some pg parameters which requires postgres restart to take effect after changing. For example, changing `max_connections` will not take effect till the underlying postgres is restarted. This is disabled by default and can be enabled using the clusterSpecification `automaticPgRestart`. This is achieved using `pending_restart` in [pg_settings](https://www.postgresql.org/docs/9.5/static/view-pg-settings.html) for postgres 9.5 and above and the `context` column of `pg_settings` for lower versions (<= 9.4). -------------------------------------------------------------------------------- /doc/service_discovery.md: -------------------------------------------------------------------------------- 1 | ## Service Discovery 2 | 3 | Service discovery helps in discovering stolon standby keepers for performing readonly queries 4 | 5 | ![Stolon architecture](stolon_service_discovery.png) 6 | 7 | `register` watches the keeper and registers them in `consul` for service discovery. 8 | 9 | `clients` which want to do readonly query can find the standby keepers by querying `consul` 10 | 11 | > use `--register-master` flag to register master for performing readonly queries, don't use it for write queries, always use `proxy` to connect to master, refer [here](./faq.md#why-clients-should-use-the-stolon-proxy) for more info -------------------------------------------------------------------------------- /doc/ssl.md: -------------------------------------------------------------------------------- 1 | ## PostgreSQL SSL/TLS setup 2 | 3 | SSL/TLS access to an HA postgres managed by stolon can be configured as usual (see the [official postgres doc](https://www.postgresql.org/docs/current/static/ssl-tcp.html)). The setup is done [defining the required pgParameters inside the cluster spec](postgres_parameters.md). If this is enabled also replication between instances will use tls (currently it'll use the default replication mode of "prefer"). 4 | 5 | If you want to enable client side full verification (`sslmode=verify-full` in the client connection string) you should configure the certificate CN to contain the FQDN or IP address that your client will use to connect to the stolon proxies. Depending on your architecture you'll have more than one stolon proxies behind a load balancer, a keepealived ip, a k8s service etc... So the certificate CN should be set to the hostname or ip that your client will connect to. 6 | 7 | For the above reasons, the certificate, key and root CA files will usually be the same for every postgres instance (primary or standbys) so they can be put inside the PGDATA directory (so they could be automatically copied to new instances during a resync) or also outside it (in this case you should ensure that they exists and are available to the postgres instance). 8 | -------------------------------------------------------------------------------- /doc/standbycluster.md: -------------------------------------------------------------------------------- 1 | ## Stolon standby cluster 2 | 3 | A stolon cluster can be initialized as a standby of another remote postgresql instance (being it another stolon cluster or a standalone instance or any other kind of architecture). 4 | 5 | This is useful for a lot of different use cases: 6 | 7 | * Disaster recovery 8 | * (near) Zero downtime migration to stolon 9 | 10 | In a stolon standby cluster the master keeper will be the one that will sync with the remote instance, while the other keepers will replicate with the master keeper (creating a cascading replication topology). 11 | Everything else will work as a normal cluster, if a keeper dies another one will be elected as the cluster master. 12 | 13 | #### A standby cluster of a standalone postgres instance 14 | ![Standby Cluster](standbycluster_standalone_small.png) 15 | 16 | #### A standby cluster of another stolon cluster 17 | ![Standby Cluster](standbycluster_small.png) 18 | 19 | 20 | ### Initializing a stolon standby cluster 21 | 22 | #### Prerequisites 23 | 24 | * The remote postgresql primary should have defined a superuser and user with replication privileges (can also be the same superuser) and accept remote logins from the replication user (be sure `pg_hba.conf` contains the required lines). 25 | * You should provide the above user credentials to the stolon keepers (`--pg-su-username --pg-su-passwordfile/--pg-su-password --pg-repl-username --pg-repl-passwordfile/--pg-repl-password`) 26 | 27 | **NOTE:** In future we could improve this example using other authentication methods like client TLS certificates. 28 | 29 | In this example we'll use the below information: 30 | 31 | * remote instance host: `remoteinstancehost` 32 | * remote instance port: `5432` 33 | * remote instance replication user name: `repluser` 34 | * remote instance replication user password: `replpassword` 35 | * stolon cluster name: `stolon-cluster` 36 | * stolon store type: `etcdv3` (listening on localhost with default port to make it simple) 37 | 38 | We can leverage stolon [Point in time Recovery](pitr.md) feature to clone from a remote postgres db. For example we can use `pg_basebackup` to initialize the cluster. We have to call `pg_basebackup` providing the remote instance credential for a replication user. To provide the password to `pg_basebackup` we have to create a password file like this: 39 | 40 | ``` 41 | remoteinstancehost:5432:*:repluser:replpassword 42 | ``` 43 | 44 | ensure to set the [right permissions to the password file](https://www.postgresql.org/docs/current/static/libpq-pgpass.html). 45 | 46 | 47 | * Start one or more stolon sentinels and one or more stolon keepers passing the right values for `--pg-su-username --pg-su-passwordfile/--pg-su-password --pg-repl-username --pg-repl-passwordfile/--pg-repl-password` 48 | 49 | * Initialize the cluster with the following cluster spec: 50 | 51 | ``` 52 | stolonctl --cluster-name stolon-cluster --store-backend=etcdv3 init ' 53 | { 54 | "role": "standby", 55 | "initMode": "pitr", 56 | "pitrConfig": { 57 | "dataRestoreCommand": "PGPASSFILE=passfile pg_basebackup -D \"%d\" -h remoteinstancehost -p 5432 -U repluser" 58 | }, 59 | "standbyConfig": { 60 | "standbySettings": { 61 | "primaryConnInfo": "host=remoteinstancehost port=5432 user=repluser password=replpassword sslmode=disable" 62 | } 63 | } 64 | }' 65 | ``` 66 | 67 | If all is correct the sentinel will choose a keeper as the cluster "master" and this keeper will start the db pitr using the `pg_basebackup` command provided in the cluster specification. 68 | 69 | If the command completes successfully the master keeper will start as a standby of the remote instance using the recovery options provided in the cluster spec `standbyConfig` (this will be used to generate the `recovery.conf` file). 70 | 71 | The other keepers will become standby keepers of the cluster master keeper. 72 | 73 | ### Additional options 74 | 75 | You can specify additional options in the `standbyConfig` (for all the options see the [cluster spec doc](https://github.com/sorintlab/stolon/blob/master/doc/cluster_spec.md#standbyconfig)) 76 | 77 | For example you can specify a primary slot name to use for syncing with the master and a wal apply delay 78 | 79 | Ex. with a primary slot name: 80 | ``` 81 | "standbyConfig": { 82 | "standbySettings": { 83 | "primaryConnInfo": "host=remoteinstancehost port=5432 user=repluser password=replpassword sslmode=disable", 84 | "primarySlotName": "standbycluster" 85 | } 86 | } 87 | ``` 88 | 89 | ### Promoting a standby cluster 90 | 91 | When you want to promote your standby cluster to a primary one (for example in a disaster recovery scenario to switch to a dr site, or during a migration to switch to the new stolon cluster) you can do this using `stolonctl`: 92 | 93 | ``` 94 | stolonctl --cluster-name stolon-cluster --store-backend=etcdv3 promote 95 | ``` 96 | 97 | This is the same as doing: 98 | 99 | ``` 100 | stolonctl --cluster-name stolon-cluster --store-backend=etcdv3 update --patch { "role": "master" } 101 | ``` 102 | -------------------------------------------------------------------------------- /doc/standbycluster_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/standbycluster_small.png -------------------------------------------------------------------------------- /doc/standbycluster_standalone_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/standbycluster_standalone_small.png -------------------------------------------------------------------------------- /doc/stolon_service_discovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/stolon_service_discovery.png -------------------------------------------------------------------------------- /doc/stolonctl.md: -------------------------------------------------------------------------------- 1 | ## Stolon Client (stolonctl) 2 | 3 | `stolonctl` is the stolon client which controls the stolon cluster(s). 4 | 5 | Since `stolonctl`needs to communicate with the cluster backend store, it requires providing the requested cluster name (`--cluster-name`), its store backend type (`--store-backend`), and how to reach the store, such as: 6 | * For etcdv2, etcdv3 or consul as store, a comma separated list of endpoints (`--store-endpoints`). 7 | * For kubernetes as store, the kind of kubernetes resources (`--kube-resource-kind`). See below. 8 | 9 | `stolonctl` example for checking the status of a cluster named "stolon-cluster" using "etcdv3" as a store backend: 10 | ``` 11 | $ stolonctl --cluster-name=stolon-cluster --store-backend=etcdv3 --store-endpoints=http://etcd-0:2379,http://etcd-1:2379,http://etcd-2:2379 status 12 | ``` 13 | 14 | Note: To avoid repeating the arguments on every command (or inside scripts), all the options can be exported as environment variables. Their name will be the same as the option name converted in uppercase, with `_` replacing `-` and prefixed with `STOLONCTL_`. 15 | 16 | For example: 17 | ``` 18 | STOLONCTL_STORE_BACKEND 19 | STOLONCTL_STORE_ENDPOINTS 20 | STOLONCTL_CLUSTER_NAME 21 | ``` 22 | 23 | ### Running in Kubernetes 24 | 25 | `stolonctl` behaves like [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) when choosing how to access the kubernetes API server(s): 26 | * When run inside a pod it uses the pod service account to connect to the k8s API servers. 27 | * When run externally it honors the $KUBECONFIG environment variable to connect. It is thus possible to use the default `~/.kube/config` file or an overriden kube-config file path, context and namespace to set the `stolonctl` options `--kubeconfig`, `--kube-context` and `--kube-namespace`. 28 | 29 | `stolonctl` example for checking the status of a cluster named "kube-stolon" using "kubernetes" as a store backend and "configmap" as the resource kind where the `stolonctl` command is invoked via one of the stolon proxy pods: 30 | ``` 31 | $kubectl exec -i -t stolon-proxy-669f7b54fd-9psm2 -- stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap status 32 | ``` 33 | 34 | Same `stolonctl` command as a one shot: 35 | ``` 36 | kubectl run -i -t stolonctl --image=sorintlab/stolon:master-pg9.6 --restart=Never --rm -- /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap status 37 | ``` 38 | 39 | ### See also 40 | 41 | [stolonctl command invocation](commands/stolonctl.md) 42 | -------------------------------------------------------------------------------- /doc/twonodes.md: -------------------------------------------------------------------------------- 1 | # Two (or more) nodes setup example 2 | 3 | If you're going to run stolon on an IaaS or any kind of two nodes (baremetal, VM) this document provides some example configurations. These examples provide a two nodes setup that can be extended to a number greater than two nodes. 4 | 5 | The related store should live outside the instances hosting the stolon cluster for multiple reasons: 6 | 7 | * Stores like etcd/consul uses the raft consensus protocol that requires at least 3 nodes to provides high availability (it can survive to the death of (N/2)-1 members) 8 | * Disk I/O of postgres must not impact the etcd/consul I/O 9 | * You can run multiple stolon clusters using the same store 10 | 11 | # Two nodes with load balancer 12 | 13 | ![Two nodes with a load balancer](twonodes_lb_small.png) 14 | 15 | Every nodes will run one keeper, one sentinel and one proxy. 16 | 17 | The load balancer will balance between the stolon proxies. 18 | 19 | Clients will connect to the load balancer VIP. 20 | 21 | # Two nodes with keepalived 22 | 23 | ![Two nodes with keepalived](twonodes_keepalived_small.png) 24 | 25 | Every nodes will run one keeper, one sentinel and one proxy. 26 | 27 | Every nodes also runs a [keepalived](http://www.keepalived.org/) instance that will mantain a VIP assigned to one of the nodes. If one of the nodes dies/become partitioned keepealived will switch the VIP to the other node. 28 | 29 | Clients will connect to the VIP managed by keepalived. 30 | -------------------------------------------------------------------------------- /doc/twonodes_keepalived_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/twonodes_keepalived_small.png -------------------------------------------------------------------------------- /doc/twonodes_lb_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/doc/twonodes_lb_small.png -------------------------------------------------------------------------------- /doc/unofficial_packages.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | For Mac and Linux we can use [brew](https://brew.sh/), 3 | 4 | ``` 5 | brew install stolon 6 | ``` 7 | -------------------------------------------------------------------------------- /doc/upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrading from v0.4.0 to v0.5.0 2 | 3 | ## Removed commands options 4 | 5 | These stolon commands options were removed. You should update your scripts invoking the stolon components removing them. 6 | 7 | ### stolon-keeper 8 | `--listen-address` 9 | `--port` 10 | `--pg-conf-dir` 11 | `--id` has been deprecated (but yet available). `--uid` should be used instead. 12 | 13 | ### stolon-sentinel 14 | `--listen-address` 15 | `--port` 16 | `--discovery-type` 17 | `--initial-cluster-config` (the equivalent for the new cluster spec format is `--initial-cluster-spec`) 18 | `--keeper-kube-label-selector` 19 | `--keeper-port` 20 | `--kubernetes-namespace` 21 | 22 | ### Upgrade for new cluster data 23 | 24 | Stolon v0.5.0 received a big rework to improve its internal data model and implement new features. To upgrade an existing cluster from v0.4.0 to v0.5.0 you can follow the steps below (we suggest to try them in a test environment). 25 | 26 | * Annotate the master keeperUID (previously called keeper id). You can retrieve this using `stolonctl status`. 27 | * Stop all the cluster processes (keepers, sentinels and proxies) 28 | * Upgrade the binaries to stolon v0.5.0 29 | * Relaunch all the cluster processes. They will loop reporting `unsupported clusterdata format version 0`. 30 | * Initialize a new cluster data using the master keeperUID: 31 | 32 | ``` 33 | stolonctl init '{ "initMode": "existing", "existingConfig": { "keeperUID": "keeper01" } }' 34 | ``` 35 | 36 | The leader sentinel will choose the other keepers as standbys and they'll resync with the current master (they will do this also if before the upgrade they were already standbys since this is needed to adapt to the new cluster data format). 37 | -------------------------------------------------------------------------------- /examples/kubernetes/image/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PGVERSION 2 | 3 | # base build image 4 | FROM golang:1.16-buster AS build_base 5 | 6 | WORKDIR /stolon 7 | 8 | # only copy go.mod and go.sum 9 | COPY go.mod . 10 | COPY go.sum . 11 | 12 | RUN go mod download 13 | 14 | ####### 15 | ####### Build the stolon binaries 16 | ####### 17 | FROM build_base AS builder 18 | 19 | # copy all the sources 20 | COPY . . 21 | 22 | RUN make 23 | 24 | ####### 25 | ####### Build the final image 26 | ####### 27 | FROM postgres:$PGVERSION 28 | 29 | RUN useradd -ms /bin/bash stolon 30 | 31 | EXPOSE 5432 32 | 33 | # copy the agola-web dist 34 | COPY --from=builder /stolon/bin/ /usr/local/bin 35 | 36 | RUN chmod +x /usr/local/bin/stolon-keeper /usr/local/bin/stolon-sentinel /usr/local/bin/stolon-proxy /usr/local/bin/stolonctl 37 | -------------------------------------------------------------------------------- /examples/kubernetes/postgresql_upgrade.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL upgrade 2 | 3 | This is an example of upgrading a stolon cluster from pg9.6 to pg10.0. It use pg_upgrade to 4 | perform the upgrade. 5 | 6 | An alternative way to upgrade would be to dump (pg_dump) and restore the database on a new 7 | stolon cluster. This may be easier to perform, but on large database result in longer downtime. 8 | 9 | The tricky part of the upgrade, is that pg_upgrade require both PostgreSQL 9.6 and 10 | PostgreSQL 10.0 binary to do the upgrade. Major upgrade of PostgreSQL on Docker is discussed 11 | on https://github.com/docker-library/postgres/issues/37, this example use the 12 | [tianon/docker-postgres-upgrade](https://github.com/tianon/docker-postgres-upgrade) solution. 13 | 14 | 15 | ## Upgrade with pg_upgrade 16 | 17 | As usual, before processing with major upgrade, it is recommended to perform a backup of the database. 18 | 19 | pg_upgrade require exclusive access to data files, shutdown the old PostgreSQL server. 20 | 21 | ``` 22 | kubectl delete -f stolon-keeper.yaml 23 | ``` 24 | 25 | Note: since stolon-keeper.yaml only contains the StatefulSet/stolon-keeper object, only this objects 26 | and the created pods will be deleted. Not the persistent volume claim that contains data. 27 | 28 | Run a pod with `tianon/postgres-upgrade:9.6-to-10` on keeper-0 data and attach it: 29 | 30 | ``` 31 | cat << EOF | kubectl create -f - 32 | kind: Pod 33 | apiVersion: v1 34 | metadata: 35 | name: stolon-upgrade 36 | spec: 37 | volumes: 38 | - name: data-stolon-keeper-0 39 | persistentVolumeClaim: 40 | claimName: data-stolon-keeper-0 41 | containers: 42 | - name: stolon-upgrade 43 | args: 44 | - bash 45 | stdin: true 46 | tty: true 47 | image: tianon/postgres-upgrade:9.6-to-10 48 | volumeMounts: 49 | - mountPath: "/stolon-data" 50 | name: data-stolon-keeper-0 51 | EOF 52 | 53 | kubectl attach -ti stolon-upgrade 54 | ``` 55 | 56 | Inside this stolon-upgrade pod, create a stolon user that will run pg_upgrade (pg_upgrade refuse 57 | to run as root): 58 | 59 | ``` 60 | useradd --uid 1000 stolon 61 | gosu stolon bash 62 | ``` 63 | 64 | pg_upgrade work by "copying" data from old PostgreSQL to new PostgreSQL (applying required 65 | change). It needs two PostgreSQL data folder. When option `--link` is used pg_upgrade will 66 | not copy data but use hard-link to speed up process. Refer to 67 | [pg_upgrade documentation](https://www.postgresql.org/docs/current/static/pgupgrade.html) for 68 | more details. 69 | 70 | Create the new PostgreSQL data folder: 71 | 72 | ``` 73 | PGDATA=/stolon-data/postgres-new initdb 74 | ``` 75 | 76 | pg_upgrade will start old and new database and require access to them. Use new pg_hba to allow 77 | local access for pg_upgrade: 78 | 79 | ``` 80 | cp /stolon-data/postgres-new/pg_hba.conf /stolon-data/postgres/pg_hba.conf 81 | ``` 82 | 83 | Run pg_upgrade 84 | 85 | ``` 86 | cd /tmp 87 | pg_upgrade -d /stolon-data/postgres -D /stolon-data/postgres-new --link 88 | ``` 89 | 90 | Move postgres-new folder in place of postgres folder: 91 | 92 | ``` 93 | rm -fr /stolon-data/postgres 94 | mv /stolon-data/postgres-new/ /stolon-data/postgres 95 | ``` 96 | 97 | pg_upgrade said that two script were generated (at least for 9.6 to 10.0): 98 | 99 | * One to remove old data: it was done by the above move 100 | * Another to update statistics (using vacuumdb once PostgreSQL will be running). This 101 | will be run later. 102 | 103 | Exit and delete this stolon-upgrade pod: 104 | 105 | ``` 106 | kubectl detele pod stolon-upgrade 107 | ``` 108 | 109 | For all other data volume of stolon-keeper, run the same step. Just update the yaml used in 110 | kubectl create. Tip: `kubectl get pvc` will list all persistent volume claim, thus all 111 | data volume that needs update. 112 | 113 | 114 | Once all data volume are updated, re-create the stolon-keeper using PostgreSQL 10 image: 115 | 116 | ``` 117 | sed -i 's/stolon:master-pg9.6/stolon:master-pg10.0/' stolon-keeper.yaml 118 | kubectl create -f stolon-keeper.yaml 119 | ``` 120 | 121 | The cluster should quickly resume its operation. Once done, find the current master and 122 | run the vaccumdb as pg_upgrade said: 123 | 124 | ``` 125 | kubectl exec -ti stolon-keeper-0 bash # this assume -0 is the master 126 | su - stolon 127 | vacuumdb --all --analyze-in-stages -h 127.0.0.1 -U stolon 128 | ``` 129 | -------------------------------------------------------------------------------- /examples/kubernetes/role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: stolon 5 | namespace: default 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: stolon 10 | subjects: 11 | - kind: ServiceAccount 12 | name: default 13 | namespace: default 14 | -------------------------------------------------------------------------------- /examples/kubernetes/role.yaml: -------------------------------------------------------------------------------- 1 | # This is an example and generic rbac role definition for stolon. It could be 2 | # fine tuned and split per component. 3 | # The required permission per component should be: 4 | # keeper/proxy/sentinel: update their own pod annotations 5 | # sentinel/stolonctl: get, create, update configmaps 6 | # sentinel/stolonctl: list components pods 7 | # sentinel/stolonctl: get components pods annotations 8 | 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: Role 11 | metadata: 12 | name: stolon 13 | namespace: default 14 | rules: 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - pods 19 | - configmaps 20 | - events 21 | verbs: 22 | - "*" 23 | -------------------------------------------------------------------------------- /examples/kubernetes/secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: stolon 6 | type: Opaque 7 | data: 8 | password: cGFzc3dvcmQx 9 | -------------------------------------------------------------------------------- /examples/kubernetes/stolon-keeper.yaml: -------------------------------------------------------------------------------- 1 | # PetSet was renamed to StatefulSet in k8s 1.5 2 | # apiVersion: apps/v1alpha1 3 | # kind: PetSet 4 | apiVersion: apps/v1 5 | kind: StatefulSet 6 | metadata: 7 | name: stolon-keeper 8 | spec: 9 | serviceName: "stolon-keeper" 10 | replicas: 2 11 | selector: 12 | matchLabels: 13 | component: stolon-keeper 14 | stolon-cluster: kube-stolon 15 | template: 16 | metadata: 17 | labels: 18 | component: stolon-keeper 19 | stolon-cluster: kube-stolon 20 | annotations: 21 | pod.alpha.kubernetes.io/initialized: "true" 22 | prometheus.io/scrape: "true" 23 | prometheus.io/port: "8080" 24 | spec: 25 | terminationGracePeriodSeconds: 10 26 | containers: 27 | - name: stolon-keeper 28 | image: sorintlab/stolon:master-pg10 29 | command: 30 | - "/bin/bash" 31 | - "-ec" 32 | - | 33 | # Generate our keeper uid using the pod index 34 | IFS='-' read -ra ADDR <<< "$(hostname)" 35 | export STKEEPER_UID="keeper${ADDR[-1]}" 36 | export POD_IP=$(hostname -i) 37 | export STKEEPER_PG_LISTEN_ADDRESS=$POD_IP 38 | export STOLON_DATA=/stolon-data 39 | chown stolon:stolon $STOLON_DATA 40 | exec gosu stolon stolon-keeper --data-dir $STOLON_DATA 41 | env: 42 | - name: POD_NAME 43 | valueFrom: 44 | fieldRef: 45 | fieldPath: metadata.name 46 | - name: STKEEPER_CLUSTER_NAME 47 | valueFrom: 48 | fieldRef: 49 | fieldPath: metadata.labels['stolon-cluster'] 50 | - name: STKEEPER_STORE_BACKEND 51 | value: "kubernetes" 52 | - name: STKEEPER_KUBE_RESOURCE_KIND 53 | value: "configmap" 54 | - name: STKEEPER_PG_REPL_USERNAME 55 | value: "repluser" 56 | # Or use a password file like in the below supersuser password 57 | - name: STKEEPER_PG_REPL_PASSWORD 58 | value: "replpassword" 59 | - name: STKEEPER_PG_SU_USERNAME 60 | value: "stolon" 61 | - name: STKEEPER_PG_SU_PASSWORDFILE 62 | value: "/etc/secrets/stolon/password" 63 | - name: STKEEPER_METRICS_LISTEN_ADDRESS 64 | value: "0.0.0.0:8080" 65 | # Uncomment this to enable debug logs 66 | #- name: STKEEPER_DEBUG 67 | # value: "true" 68 | ports: 69 | - containerPort: 5432 70 | - containerPort: 8080 71 | volumeMounts: 72 | - mountPath: /stolon-data 73 | name: data 74 | - mountPath: /etc/secrets/stolon 75 | name: stolon 76 | volumes: 77 | - name: stolon 78 | secret: 79 | secretName: stolon 80 | # Define your own volumeClaimTemplate. This example uses dynamic PV provisioning with a storage class named "standard" (so it will works by default with minikube) 81 | # In production you should use your own defined storage-class and configure your persistent volumes (statically or dynamically using a provisioner, see related k8s doc). 82 | volumeClaimTemplates: 83 | - metadata: 84 | name: data 85 | annotations: 86 | volume.alpha.kubernetes.io/storage-class: standard 87 | spec: 88 | accessModes: ["ReadWriteOnce"] 89 | resources: 90 | requests: 91 | storage: 512Mi 92 | -------------------------------------------------------------------------------- /examples/kubernetes/stolon-proxy-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: stolon-proxy-service 5 | spec: 6 | ports: 7 | - port: 5432 8 | targetPort: 5432 9 | selector: 10 | component: stolon-proxy 11 | stolon-cluster: kube-stolon 12 | -------------------------------------------------------------------------------- /examples/kubernetes/stolon-proxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stolon-proxy 5 | spec: 6 | replicas: 2 7 | selector: 8 | matchLabels: 9 | component: stolon-proxy 10 | stolon-cluster: kube-stolon 11 | template: 12 | metadata: 13 | labels: 14 | component: stolon-proxy 15 | stolon-cluster: kube-stolon 16 | annotations: 17 | prometheus.io/scrape: "true" 18 | prometheus.io/port: "8080" 19 | spec: 20 | containers: 21 | - name: stolon-proxy 22 | image: sorintlab/stolon:master-pg10 23 | command: 24 | - "/bin/bash" 25 | - "-ec" 26 | - | 27 | exec gosu stolon stolon-proxy 28 | env: 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: STPROXY_CLUSTER_NAME 34 | valueFrom: 35 | fieldRef: 36 | fieldPath: metadata.labels['stolon-cluster'] 37 | - name: STPROXY_STORE_BACKEND 38 | value: "kubernetes" 39 | - name: STPROXY_KUBE_RESOURCE_KIND 40 | value: "configmap" 41 | - name: STPROXY_LISTEN_ADDRESS 42 | value: "0.0.0.0" 43 | - name: STPROXY_METRICS_LISTEN_ADDRESS 44 | value: "0.0.0.0:8080" 45 | ## Uncomment this to enable debug logs 46 | #- name: STPROXY_DEBUG 47 | # value: "true" 48 | ports: 49 | - containerPort: 5432 50 | - containerPort: 8080 51 | readinessProbe: 52 | tcpSocket: 53 | port: 5432 54 | initialDelaySeconds: 10 55 | timeoutSeconds: 5 56 | -------------------------------------------------------------------------------- /examples/kubernetes/stolon-sentinel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stolon-sentinel 5 | spec: 6 | replicas: 2 7 | selector: 8 | matchLabels: 9 | component: stolon-sentinel 10 | stolon-cluster: kube-stolon 11 | template: 12 | metadata: 13 | labels: 14 | component: stolon-sentinel 15 | stolon-cluster: kube-stolon 16 | annotations: 17 | prometheus.io/scrape: "true" 18 | prometheus.io/port: "8080" 19 | spec: 20 | containers: 21 | - name: stolon-sentinel 22 | image: sorintlab/stolon:master-pg10 23 | command: 24 | - "/bin/bash" 25 | - "-ec" 26 | - | 27 | exec gosu stolon stolon-sentinel 28 | env: 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: STSENTINEL_CLUSTER_NAME 34 | valueFrom: 35 | fieldRef: 36 | fieldPath: metadata.labels['stolon-cluster'] 37 | - name: STSENTINEL_STORE_BACKEND 38 | value: "kubernetes" 39 | - name: STSENTINEL_KUBE_RESOURCE_KIND 40 | value: "configmap" 41 | - name: STSENTINEL_METRICS_LISTEN_ADDRESS 42 | value: "0.0.0.0:8080" 43 | ## Uncomment this to enable debug logs 44 | #- name: STSENTINEL_DEBUG 45 | # value: "true" 46 | ports: 47 | - containerPort: 8080 48 | -------------------------------------------------------------------------------- /examples/openshift/README.md: -------------------------------------------------------------------------------- 1 | # Stolon inside openshift 2 | 3 | Compared to the kubernetes deployments few changes are required. The purpose of this document is to cover these additional steps. 4 | 5 | ## Service account 6 | The best approch to deploy Stolon in openshift is to create a dedicate service account and assign it the required Security context constraints (scc). 7 | ``` 8 | oc create sa 9 | ``` 10 | stolon_role.yaml and role-binding.yaml must be modified to match service account name and the namespace for the cluster deployment 11 | 12 | As additional step assign anyuid scc to the service account: 13 | ``` 14 | oc adm policy add-scc-to-user anyuid system:serviceaccount:: 15 | ``` 16 | ## Patch cluster components: 17 | Sentinel: 18 | ``` 19 | oc patch --local=true -f stolon-sentinel.yaml -p '{"spec":{"template":{"spec":{"serviceAccount": ""}}}}' -o yaml > stolon-sentinel_new.yaml 20 | ``` 21 | Keeper 22 | ``` 23 | oc patch --local=true -f stolon-keeper.yaml -p '{"spec":{"template":{"spec":{"serviceAccount": ""}}}}' -o yaml > stolon-keeper_new.yaml 24 | ``` 25 | Proxy 26 | ``` 27 | oc patch --local=true -f stolon-proxy.yaml -p '{"spec":{"template":{"spec":{"serviceAccount": ""}}}}' -o yaml > stolon-proxy_new.yaml 28 | ``` 29 | ## Deployment 30 | Deploy the Stolon components using the files created at the previous step. 31 | -------------------------------------------------------------------------------- /examples/swarm/docker-compose-etcd.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | etcd-00: 5 | image: quay.io/coreos/etcd:v3.2.17 6 | hostname: etcd-00 7 | command: 8 | - etcd 9 | - --name=etcd-00 10 | - --data-dir=data.etcd 11 | - --advertise-client-urls=http://etcd-00:2379 12 | - --listen-client-urls=http://0.0.0.0:2379 13 | - --initial-advertise-peer-urls=http://etcd-00:2380 14 | - --listen-peer-urls=http://0.0.0.0:2380 15 | - --initial-cluster=etcd-00=http://etcd-00:2380,etcd-01=http://etcd-01:2380,etcd-02=http://etcd-02:2380 16 | - --initial-cluster-state=new 17 | - --initial-cluster-token=${ETCD_TOKEN} 18 | volumes: 19 | - etcd-00vol:/data.etcd 20 | networks: 21 | - etcd 22 | ports: 23 | - 2379:2379 24 | deploy: 25 | replicas: 1 26 | 27 | etcd-01: 28 | image: quay.io/coreos/etcd:v3.2.17 29 | hostname: etcd-01 30 | command: 31 | - etcd 32 | - --name=etcd-01 33 | - --data-dir=data.etcd 34 | - --advertise-client-urls=http://etcd-01:2379 35 | - --listen-client-urls=http://0.0.0.0:2379 36 | - --initial-advertise-peer-urls=http://etcd-01:2380 37 | - --listen-peer-urls=http://0.0.0.0:2380 38 | - --initial-cluster=etcd-00=http://etcd-00:2380,etcd-01=http://etcd-01:2380,etcd-02=http://etcd-02:2380 39 | - --initial-cluster-state=new 40 | - --initial-cluster-token=${ETCD_TOKEN} 41 | volumes: 42 | - etcd-01vol:/data.etcd 43 | networks: 44 | - etcd 45 | deploy: 46 | replicas: 1 47 | 48 | etcd-02: 49 | image: quay.io/coreos/etcd:v3.2.17 50 | hostname: etcd-02 51 | command: 52 | - etcd 53 | - --name=etcd-02 54 | - --data-dir=data.etcd 55 | - --advertise-client-urls=http://etcd-02:2379 56 | - --listen-client-urls=http://0.0.0.0:2379 57 | - --initial-advertise-peer-urls=http://etcd-02:2380 58 | - --listen-peer-urls=http://0.0.0.0:2380 59 | - --initial-cluster=etcd-00=http://etcd-00:2380,etcd-01=http://etcd-01:2380,etcd-02=http://etcd-02:2380 60 | - --initial-cluster-state=new 61 | - --initial-cluster-token=${ETCD_TOKEN} 62 | volumes: 63 | - etcd-02vol:/data.etcd 64 | networks: 65 | - etcd 66 | deploy: 67 | replicas: 1 68 | 69 | volumes: 70 | etcd-00vol: 71 | driver: local 72 | etcd-01vol: 73 | driver: local 74 | etcd-02vol: 75 | driver: local 76 | 77 | networks: 78 | etcd: 79 | driver: overlay 80 | driver_opts: 81 | encrypted: "true" 82 | internal: true 83 | -------------------------------------------------------------------------------- /examples/swarm/docker-compose-pg.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | secrets: 4 | pgsql: 5 | file: ./etc/secrets/pgsql 6 | pgsql_repl: 7 | file: ./etc/secrets/pgsql_repl 8 | 9 | services: 10 | sentinel: 11 | image: sorintlab/stolon:master-pg10 12 | command: gosu stolon stolon-sentinel --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level debug 13 | networks: 14 | - etcd_etcd 15 | - pgdb 16 | deploy: 17 | replicas: 2 18 | update_config: 19 | parallelism: 1 20 | delay: 30s 21 | order: stop-first 22 | failure_action: pause 23 | 24 | keeper1: 25 | image: sorintlab/stolon:master-pg10 26 | hostname: keeper1 27 | environment: 28 | - PGDATA=/var/lib/postgresql/data 29 | volumes: 30 | - pgkeeper1:/var/lib/postgresql/data 31 | secrets: 32 | - pgsql 33 | - pgsql_repl 34 | command: gosu stolon stolon-keeper --pg-listen-address keeper1 --pg-repl-username replication --uid keeper1 --pg-su-username postgres --pg-su-passwordfile /run/secrets/pgsql --pg-repl-passwordfile /run/secrets/pgsql_repl --data-dir /var/lib/postgresql/data --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level debug 35 | networks: 36 | - etcd_etcd 37 | - pgdb 38 | deploy: 39 | replicas: 1 40 | # placement: 41 | # constraints: [node.labels.nodename == node1] 42 | 43 | keeper2: 44 | image: sorintlab/stolon:master-pg10 45 | hostname: keeper2 46 | environment: 47 | - PGDATA=/var/lib/postgresql/data 48 | volumes: 49 | - pgkeeper2:/var/lib/postgresql/data 50 | secrets: 51 | - pgsql 52 | command: gosu stolon stolon-keeper --pg-listen-address keeper2 --pg-repl-username replication --uid keeper2 --pg-su-username postgres --pg-su-passwordfile /run/secrets/pgsql --pg-repl-passwordfile /run/secrets/pgsql --data-dir /var/lib/postgresql/data --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level debug 53 | networks: 54 | - etcd_etcd 55 | - pgdb 56 | deploy: 57 | replicas: 1 58 | # placement: 59 | # constraints: [node.labels.nodename == node2] 60 | 61 | proxy: 62 | image: sorintlab/stolon:master-pg10 63 | command: gosu stolon stolon-proxy --listen-address 0.0.0.0 --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level info 64 | networks: 65 | - etcd_etcd 66 | - pgdb 67 | ports: 68 | - 5432:5432 69 | deploy: 70 | replicas: 2 71 | update_config: 72 | parallelism: 1 73 | delay: 30s 74 | order: stop-first 75 | failure_action: rollback 76 | 77 | volumes: 78 | pgkeeper1: 79 | driver: local 80 | pgkeeper2: 81 | driver: local 82 | 83 | networks: 84 | etcd_etcd: 85 | external: true 86 | pgdb: 87 | driver: overlay 88 | driver_opts: 89 | encrypted: "true" 90 | internal: true 91 | -------------------------------------------------------------------------------- /examples/swarm/etc/secrets/pgsql: -------------------------------------------------------------------------------- 1 | password1 -------------------------------------------------------------------------------- /examples/swarm/etc/secrets/pgsql_repl: -------------------------------------------------------------------------------- 1 | password1 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sorintlab/stolon 2 | 3 | require ( 4 | github.com/coreos/bbolt v1.3.3 // indirect 5 | github.com/coreos/etcd v3.3.18+incompatible // indirect 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/docker/leadership v0.1.0 8 | github.com/docker/libkv v0.2.1 9 | github.com/evanphx/json-patch v4.5.0+incompatible 10 | github.com/gofrs/uuid v4.2.0+incompatible 11 | github.com/golang/mock v1.4.0 12 | github.com/google/go-cmp v0.4.0 13 | github.com/hashicorp/consul/api v1.4.0 14 | github.com/lib/pq v1.3.0 15 | github.com/mattn/go-isatty v0.0.12 16 | github.com/mitchellh/copystructure v1.0.0 17 | github.com/prometheus/client_golang v1.4.1 18 | github.com/sgotti/gexpect v0.0.0-20210315095146-1ec64e69809b 19 | github.com/sorintlab/pollon v0.0.0-20181009091703-248c68238c16 20 | github.com/spf13/cobra v0.0.5 21 | github.com/spf13/pflag v1.0.5 22 | go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b 23 | go.uber.org/zap v1.13.0 24 | k8s.io/api v0.17.3 25 | k8s.io/apimachinery v0.17.3 26 | k8s.io/client-go v0.17.3 27 | ) 28 | 29 | go 1.12 30 | 31 | replace github.com/coreos/bbolt v1.3.3 => github.com/etcd-io/bbolt v1.3.3 32 | -------------------------------------------------------------------------------- /internal/cluster/cluster_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 cluster 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | ) 21 | 22 | func TestValidateReplicationSlots(t *testing.T) { 23 | tests := []struct { 24 | in string 25 | err error 26 | }{ 27 | { 28 | in: "goodslotname_434432", 29 | }, 30 | { 31 | in: "badslotname-34223", 32 | err: errors.New(`wrong replication slot name: "badslotname-34223"`), 33 | }, 34 | { 35 | in: "badslotname\n", 36 | err: errors.New(`wrong replication slot name: "badslotname\n"`), 37 | }, 38 | { 39 | in: " badslotname", 40 | err: errors.New(`wrong replication slot name: " badslotname"`), 41 | }, 42 | { 43 | in: "badslotname ", 44 | err: errors.New(`wrong replication slot name: "badslotname "`), 45 | }, 46 | { 47 | in: "stolon_c874a3cb", 48 | err: errors.New(`replication slot name is reserved: "stolon_c874a3cb"`), 49 | }, 50 | } 51 | 52 | for i, tt := range tests { 53 | err := validateReplicationSlot(tt.in) 54 | 55 | if tt.err != nil { 56 | if err == nil { 57 | t.Errorf("#%d: got no error, wanted error: %v", i, tt.err) 58 | } else if tt.err.Error() != err.Error() { 59 | t.Errorf("#%d: got error: %v, wanted error: %v", i, err, tt.err) 60 | } 61 | } else { 62 | if err != nil { 63 | t.Errorf("#%d: unexpected error: %v", i, err) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func TestClusterData_FindDB(t *testing.T) { 70 | db := DB{ 71 | UID: "dbUUID", 72 | Spec: &DBSpec{KeeperUID: "sameKeeperUUID"}, 73 | } 74 | tests := []struct { 75 | name string 76 | clusterData ClusterData 77 | keeper *Keeper 78 | expectedDB *DB 79 | }{ 80 | { 81 | name: "should return nil if the clusterData is empty", 82 | clusterData: ClusterData{}, 83 | keeper: &Keeper{}, 84 | expectedDB: nil, 85 | }, 86 | { 87 | name: "should return nil if DB is not found for given keeper", 88 | clusterData: ClusterData{ 89 | DBs: map[string]*DB{ 90 | "dbUUID": &db, 91 | }, 92 | }, 93 | keeper: &Keeper{UID: "differentUUID"}, 94 | expectedDB: nil, 95 | }, 96 | { 97 | name: "should return the DB if DB is found for given keeper", 98 | clusterData: ClusterData{ 99 | DBs: map[string]*DB{ 100 | "dbUUID": &db, 101 | }, 102 | }, 103 | keeper: &Keeper{UID: "sameKeeperUUID"}, 104 | expectedDB: &db, 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | actual := tt.clusterData.FindDB(tt.keeper) 110 | if actual != tt.expectedDB { 111 | t.Errorf("Expected %v, but got %v", tt.expectedDB, actual) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/cluster/v0/member.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 v0 16 | 17 | import "github.com/sorintlab/stolon/internal/common" 18 | 19 | type KeepersInfo map[string]*KeeperInfo 20 | 21 | type KeeperInfo struct { 22 | ID string 23 | ClusterViewVersion int 24 | ListenAddress string 25 | Port string 26 | PGListenAddress string 27 | PGPort string 28 | } 29 | 30 | func (k *KeeperInfo) Copy() *KeeperInfo { 31 | if k == nil { 32 | return nil 33 | } 34 | nk := *k 35 | return &nk 36 | } 37 | 38 | type PostgresTimelinesHistory []*PostgresTimelineHistory 39 | 40 | func (tlsh PostgresTimelinesHistory) Copy() PostgresTimelinesHistory { 41 | if tlsh == nil { 42 | return nil 43 | } 44 | ntlsh := make(PostgresTimelinesHistory, len(tlsh)) 45 | copy(ntlsh, tlsh) 46 | return ntlsh 47 | } 48 | 49 | type PostgresTimelineHistory struct { 50 | TimelineID uint64 51 | SwitchPoint uint64 52 | Reason string 53 | } 54 | 55 | func (tlsh PostgresTimelinesHistory) GetTimelineHistory(id uint64) *PostgresTimelineHistory { 56 | for _, tlh := range tlsh { 57 | if tlh.TimelineID == id { 58 | return tlh 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | type PostgresState struct { 65 | Initialized bool 66 | Role common.Role 67 | SystemID string 68 | TimelineID uint64 69 | XLogPos uint64 70 | TimelinesHistory PostgresTimelinesHistory 71 | } 72 | 73 | func (p *PostgresState) Copy() *PostgresState { 74 | if p == nil { 75 | return nil 76 | } 77 | np := *p 78 | np.TimelinesHistory = p.TimelinesHistory.Copy() 79 | return &np 80 | } 81 | 82 | type KeepersDiscoveryInfo []*KeeperDiscoveryInfo 83 | 84 | type KeeperDiscoveryInfo struct { 85 | ListenAddress string 86 | Port string 87 | } 88 | 89 | type SentinelsInfo []*SentinelInfo 90 | 91 | func (s SentinelsInfo) Len() int { return len(s) } 92 | func (s SentinelsInfo) Less(i, j int) bool { return s[i].ID < s[j].ID } 93 | func (s SentinelsInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 94 | 95 | type SentinelInfo struct { 96 | ID string 97 | ListenAddress string 98 | Port string 99 | } 100 | 101 | type ProxiesInfo []*ProxyInfo 102 | 103 | func (p ProxiesInfo) Len() int { return len(p) } 104 | func (p ProxiesInfo) Less(i, j int) bool { return p[i].ID < p[j].ID } 105 | func (p ProxiesInfo) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 106 | 107 | type ProxyInfo struct { 108 | ID string 109 | ListenAddress string 110 | Port string 111 | ClusterViewVersion int 112 | } 113 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 common 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "io/ioutil" 21 | "os" 22 | "path" 23 | "reflect" 24 | "strings" 25 | 26 | "github.com/gofrs/uuid" 27 | ) 28 | 29 | const ( 30 | StorePrefix = "stolon/cluster" 31 | 32 | SentinelLeaderKey = "sentinel-leader" 33 | ) 34 | 35 | const PgUnixSocketDirectories = "/tmp" 36 | 37 | type Role string 38 | 39 | const ( 40 | RoleUndefined Role = "undefined" 41 | RoleMaster Role = "master" 42 | RoleStandby Role = "standby" 43 | ) 44 | 45 | // Roles enumerates all possible Role values 46 | var Roles = []Role{ 47 | RoleUndefined, 48 | RoleMaster, 49 | RoleStandby, 50 | } 51 | 52 | func UID() string { 53 | u := uuid.Must(uuid.NewV4()) 54 | return fmt.Sprintf("%x", u[:4]) 55 | } 56 | 57 | func UUID() string { 58 | return uuid.Must(uuid.NewV4()).String() 59 | } 60 | 61 | const ( 62 | stolonPrefix = "stolon_" 63 | ) 64 | 65 | func StolonName(name string) string { 66 | return stolonPrefix + name 67 | } 68 | 69 | func NameFromStolonName(stolonName string) string { 70 | return strings.TrimPrefix(stolonName, stolonPrefix) 71 | } 72 | 73 | func IsStolonName(name string) bool { 74 | return strings.HasPrefix(name, stolonPrefix) 75 | } 76 | 77 | type Parameters map[string]string 78 | 79 | func (s Parameters) Equals(is Parameters) bool { 80 | return reflect.DeepEqual(s, is) 81 | } 82 | 83 | // Diff returns the list of pgParameters changed(newly added, existing deleted and value changed) 84 | func (s Parameters) Diff(newParams Parameters) []string { 85 | var changedParams []string 86 | for k, v := range newParams { 87 | if val, ok := s[k]; !ok || v != val { 88 | changedParams = append(changedParams, k) 89 | } 90 | } 91 | 92 | for k := range s { 93 | if _, ok := newParams[k]; !ok { 94 | changedParams = append(changedParams, k) 95 | } 96 | } 97 | return changedParams 98 | } 99 | 100 | // WriteFileAtomicFunc atomically writes a file, it achieves this by creating a 101 | // temporary file and then moving it. writeFunc is the func that will write 102 | // data to the file. 103 | // This function is taken from 104 | // https://github.com/youtube/vitess/blob/master/go/ioutil2/ioutil.go 105 | // Copyright 2012, Google Inc. BSD-license, see licenses/LICENSE-BSD-3-Clause 106 | func WriteFileAtomicFunc(filename string, perm os.FileMode, writeFunc func(f io.Writer) error) error { 107 | dir, name := path.Split(filename) 108 | f, err := ioutil.TempFile(dir, name) 109 | if err != nil { 110 | return err 111 | } 112 | err = writeFunc(f) 113 | if err == nil { 114 | err = f.Sync() 115 | } 116 | if closeErr := f.Close(); err == nil { 117 | err = closeErr 118 | } 119 | if permErr := os.Chmod(f.Name(), perm); err == nil { 120 | err = permErr 121 | } 122 | if err == nil { 123 | err = os.Rename(f.Name(), filename) 124 | } 125 | // Any err should result in full cleanup. 126 | if err != nil { 127 | os.Remove(f.Name()) 128 | } 129 | return err 130 | } 131 | 132 | // WriteFileAtomic atomically writes a file 133 | func WriteFileAtomic(filename string, perm os.FileMode, data []byte) error { 134 | return WriteFileAtomicFunc(filename, perm, 135 | func(f io.Writer) error { 136 | _, err := f.Write(data) 137 | return err 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /internal/common/common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 common_test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/sorintlab/stolon/internal/common" 21 | "github.com/sorintlab/stolon/internal/util" 22 | ) 23 | 24 | func TestDiffReturnsChangedParams(t *testing.T) { 25 | var curParams common.Parameters = map[string]string{ 26 | "max_connections": "100", 27 | "shared_buffers": "10MB", 28 | "huge": "off", 29 | } 30 | 31 | var newParams common.Parameters = map[string]string{ 32 | "max_connections": "200", 33 | "shared_buffers": "10MB", 34 | "work_mem": "4MB", 35 | } 36 | 37 | expectedDiff := []string{"max_connections", "huge", "work_mem"} 38 | 39 | diff := curParams.Diff(newParams) 40 | 41 | if !util.CompareStringSliceNoOrder(expectedDiff, diff) { 42 | t.Errorf("Expected diff is %v, but got %v", expectedDiff, diff) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/common/tls.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 common 16 | 17 | import ( 18 | "crypto/tls" 19 | "crypto/x509" 20 | "encoding/pem" 21 | "io/ioutil" 22 | ) 23 | 24 | func NewTLSConfig(certFile, keyFile, caFile string, insecureSkipVerify bool) (*tls.Config, error) { 25 | tlsConfig := tls.Config{} 26 | 27 | // Populate root CA certs 28 | if caFile != "" { 29 | pemBytes, err := ioutil.ReadFile(caFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | roots := x509.NewCertPool() 34 | 35 | for { 36 | var block *pem.Block 37 | block, pemBytes = pem.Decode(pemBytes) 38 | if block == nil { 39 | break 40 | } 41 | cert, err := x509.ParseCertificate(block.Bytes) 42 | if err != nil { 43 | return nil, err 44 | } 45 | roots.AddCert(cert) 46 | } 47 | 48 | tlsConfig.RootCAs = roots 49 | } 50 | 51 | // Populate keypair 52 | // both must be defined 53 | if certFile != "" && keyFile != "" { 54 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 55 | if err != nil { 56 | return nil, err 57 | } 58 | tlsConfig.Certificates = []tls.Certificate{cert} 59 | } 60 | 61 | tlsConfig.InsecureSkipVerify = insecureSkipVerify 62 | 63 | return &tlsConfig, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/flagutil/env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 flagutil 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | 22 | flag "github.com/spf13/pflag" 23 | ) 24 | 25 | // SetFlagsFromEnv parses all registered flags in the given flagset, 26 | // and if they are not already set it attempts to set their values from 27 | // environment variables. Environment variables take the name of the flag but 28 | // are UPPERCASE, and any dashes are replaced by underscores. Environment 29 | // variables additionally are prefixed by the given string followed by 30 | // and underscore. For example, if prefix=PREFIX: some-flag => PREFIX_SOME_FLAG 31 | func SetFlagsFromEnv(fs *flag.FlagSet, prefix string) (err error) { 32 | alreadySet := make(map[string]bool) 33 | fs.Visit(func(f *flag.Flag) { 34 | alreadySet[f.Name] = true 35 | }) 36 | fs.VisitAll(func(f *flag.Flag) { 37 | if !alreadySet[f.Name] { 38 | key := prefix + "_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1)) 39 | val := os.Getenv(key) 40 | if val != "" { 41 | if serr := fs.Set(f.Name, val); serr != nil { 42 | err = fmt.Errorf("invalid value %q for %s: %v", val, key, serr) 43 | } 44 | } 45 | } 46 | }) 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sorint.lab 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 | // 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 log 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | 21 | "go.uber.org/zap" 22 | "go.uber.org/zap/zapcore" 23 | ) 24 | 25 | var ( 26 | s *zap.SugaredLogger 27 | sColor *zap.SugaredLogger 28 | ) 29 | 30 | // default info level 31 | var level = zap.NewAtomicLevelAt(zapcore.InfoLevel) 32 | 33 | func init() { 34 | config := zap.Config{ 35 | Level: level, 36 | Development: false, 37 | DisableStacktrace: true, 38 | Encoding: "console", 39 | EncoderConfig: zap.NewDevelopmentEncoderConfig(), 40 | OutputPaths: []string{"stderr"}, 41 | ErrorOutputPaths: []string{"stderr"}, 42 | } 43 | 44 | logger, err := config.Build() 45 | if err != nil { 46 | panic(fmt.Errorf("failed to initialize logger: %v", err)) 47 | } 48 | s = logger.Sugar() 49 | 50 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 51 | 52 | logger, err = config.Build() 53 | if err != nil { 54 | panic(fmt.Errorf("failed to initialize color logger: %v", err)) 55 | } 56 | sColor = logger.Sugar() 57 | } 58 | 59 | func SetDebug() { 60 | level.SetLevel(zapcore.DebugLevel) 61 | } 62 | 63 | func SetLevel(lvl zapcore.Level) { 64 | level.SetLevel(lvl) 65 | } 66 | 67 | func IsDebug() bool { 68 | return level.Level() == zapcore.DebugLevel 69 | } 70 | 71 | func S() *zap.SugaredLogger { 72 | return s 73 | } 74 | 75 | func StdLog() *log.Logger { 76 | return zap.NewStdLog(s.Desugar()) 77 | } 78 | 79 | func SColor() *zap.SugaredLogger { 80 | return sColor 81 | } 82 | 83 | func StdLogColor() *log.Logger { 84 | return zap.NewStdLog(sColor.Desugar()) 85 | } 86 | -------------------------------------------------------------------------------- /internal/mock/postgresql/postgresql.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: postgresql.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | postgresql "github.com/sorintlab/stolon/internal/postgresql" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockPGManager is a mock of PGManager interface 14 | type MockPGManager struct { 15 | ctrl *gomock.Controller 16 | recorder *MockPGManagerMockRecorder 17 | } 18 | 19 | // MockPGManagerMockRecorder is the mock recorder for MockPGManager 20 | type MockPGManagerMockRecorder struct { 21 | mock *MockPGManager 22 | } 23 | 24 | // NewMockPGManager creates a new mock instance 25 | func NewMockPGManager(ctrl *gomock.Controller) *MockPGManager { 26 | mock := &MockPGManager{ctrl: ctrl} 27 | mock.recorder = &MockPGManagerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockPGManager) EXPECT() *MockPGManagerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetTimelinesHistory mocks base method 37 | func (m *MockPGManager) GetTimelinesHistory(timeline uint64) ([]*postgresql.TimelineHistory, error) { 38 | ret := m.ctrl.Call(m, "GetTimelinesHistory", timeline) 39 | ret0, _ := ret[0].([]*postgresql.TimelineHistory) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetTimelinesHistory indicates an expected call of GetTimelinesHistory 45 | func (mr *MockPGManagerMockRecorder) GetTimelinesHistory(timeline interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimelinesHistory", reflect.TypeOf((*MockPGManager)(nil).GetTimelinesHistory), timeline) 47 | } 48 | -------------------------------------------------------------------------------- /internal/postgresql/control.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 postgresql 16 | 17 | import ( 18 | "encoding/binary" 19 | "os" 20 | "path/filepath" 21 | "strconv" 22 | ) 23 | 24 | func (p *Manager) GetSystemdID() (string, error) { 25 | pgControl, err := os.Open(filepath.Join(p.dataDir, "global", "pg_control")) 26 | if err != nil { 27 | return "", err 28 | } 29 | var systemID uint64 30 | err = binary.Read(pgControl, binary.LittleEndian, &systemID) 31 | if err != nil { 32 | return "", err 33 | } 34 | return strconv.FormatUint(systemID, 10), nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/store/libkv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sorint.lab 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 | // 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 store 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/docker/leadership" 21 | libkvstore "github.com/docker/libkv/store" 22 | "github.com/docker/libkv/store/consul" 23 | "github.com/docker/libkv/store/etcd" 24 | ) 25 | 26 | func init() { 27 | etcd.Register() 28 | consul.Register() 29 | } 30 | 31 | func fromLibKVStoreErr(err error) error { 32 | switch err { 33 | case libkvstore.ErrKeyNotFound: 34 | return ErrKeyNotFound 35 | case libkvstore.ErrKeyModified: 36 | return ErrKeyModified 37 | } 38 | return err 39 | } 40 | 41 | type libKVStore struct { 42 | store libkvstore.Store 43 | } 44 | 45 | func (s *libKVStore) Put(ctx context.Context, key string, value []byte, options *WriteOptions) error { 46 | var libkvOptions *libkvstore.WriteOptions 47 | if options != nil { 48 | libkvOptions = &libkvstore.WriteOptions{TTL: options.TTL} 49 | } 50 | err := s.store.Put(key, value, libkvOptions) 51 | return fromLibKVStoreErr(err) 52 | } 53 | 54 | func (s *libKVStore) Get(ctx context.Context, key string) (*KVPair, error) { 55 | pair, err := s.store.Get(key) 56 | if err != nil { 57 | return nil, fromLibKVStoreErr(err) 58 | } 59 | return &KVPair{Key: pair.Key, Value: pair.Value, LastIndex: pair.LastIndex}, nil 60 | } 61 | 62 | func (s *libKVStore) List(ctx context.Context, directory string) ([]*KVPair, error) { 63 | pairs, err := s.store.List(directory) 64 | if err != nil { 65 | return nil, fromLibKVStoreErr(err) 66 | } 67 | kvPairs := make([]*KVPair, len(pairs)) 68 | for i, p := range pairs { 69 | kvPairs[i] = &KVPair{Key: p.Key, Value: p.Value, LastIndex: p.LastIndex} 70 | } 71 | return kvPairs, nil 72 | } 73 | 74 | func (s *libKVStore) AtomicPut(ctx context.Context, key string, value []byte, previous *KVPair, options *WriteOptions) (*KVPair, error) { 75 | var libkvPrevious *libkvstore.KVPair 76 | if previous != nil { 77 | libkvPrevious = &libkvstore.KVPair{Key: previous.Key, LastIndex: previous.LastIndex} 78 | } 79 | var libkvOptions *libkvstore.WriteOptions 80 | if options != nil { 81 | libkvOptions = &libkvstore.WriteOptions{TTL: options.TTL} 82 | } 83 | _, pair, err := s.store.AtomicPut(key, value, libkvPrevious, libkvOptions) 84 | if err != nil { 85 | return nil, fromLibKVStoreErr(err) 86 | } 87 | return &KVPair{Key: pair.Key, Value: pair.Value, LastIndex: pair.LastIndex}, nil 88 | } 89 | 90 | func (s *libKVStore) Delete(ctx context.Context, key string) error { 91 | return fromLibKVStoreErr(s.store.Delete(key)) 92 | } 93 | 94 | func (s *libKVStore) Close() error { 95 | s.store.Close() 96 | return nil 97 | } 98 | 99 | type libkvElection struct { 100 | store *libKVStore 101 | path string 102 | candidate *leadership.Candidate 103 | } 104 | 105 | func (e *libkvElection) RunForElection() (<-chan bool, <-chan error) { 106 | return e.candidate.RunForElection() 107 | } 108 | 109 | func (e *libkvElection) Stop() { 110 | e.candidate.Stop() 111 | } 112 | 113 | func (e *libkvElection) Leader() (string, error) { 114 | pair, err := e.store.Get(context.TODO(), e.path) 115 | if err != nil { 116 | if err != ErrKeyNotFound { 117 | return "", err 118 | } 119 | return "", nil 120 | } 121 | return string(pair.Value), nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/store/store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Sorint.lab 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 | // 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 store 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "time" 21 | 22 | "github.com/sorintlab/stolon/internal/cluster" 23 | ) 24 | 25 | //go:generate mockgen -destination=../mock/store/store.go -source=$GOFILE 26 | 27 | var ( 28 | // ErrKeyNotFound is thrown when the key is not found in the store during a Get operation 29 | ErrKeyNotFound = errors.New("Key not found in store") 30 | ErrKeyModified = errors.New("Unable to complete atomic operation, key modified") 31 | ErrElectionNoLeader = errors.New("election: no leader") 32 | ) 33 | 34 | type Store interface { 35 | AtomicPutClusterData(ctx context.Context, cd *cluster.ClusterData, previous *KVPair) (*KVPair, error) 36 | PutClusterData(ctx context.Context, cd *cluster.ClusterData) error 37 | GetClusterData(ctx context.Context) (*cluster.ClusterData, *KVPair, error) 38 | SetKeeperInfo(ctx context.Context, id string, ms *cluster.KeeperInfo, ttl time.Duration) error 39 | GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, error) 40 | SetSentinelInfo(ctx context.Context, si *cluster.SentinelInfo, ttl time.Duration) error 41 | GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo, error) 42 | SetProxyInfo(ctx context.Context, pi *cluster.ProxyInfo, ttl time.Duration) error 43 | GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, error) 44 | } 45 | 46 | type Election interface { 47 | // TODO(sgotti) this mimics the current docker/leadership API and the etcdv3 48 | // implementations adapt to it. In future it could be replaced with a better 49 | // api like the current one implemented by etcdclientv3/concurrency. 50 | // 51 | // WARNING: If the election error channel receives any error, it is vital that 52 | // the consuming code calls election.Stop(). Failure to do so can cause 53 | // subsequent elections to hang indefinitely across all participants of an 54 | // election. 55 | RunForElection() (<-chan bool, <-chan error) 56 | Leader() (string, error) 57 | Stop() 58 | } 59 | -------------------------------------------------------------------------------- /internal/timer/timer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 timer 16 | 17 | import "time" 18 | 19 | // Since return the duration since the starting time 20 | func Since(start int64) time.Duration { 21 | return time.Duration(Now() - start) 22 | } 23 | -------------------------------------------------------------------------------- /internal/timer/timer_fallback.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 | // +build !linux 16 | 17 | package timer 18 | 19 | import ( 20 | "time" 21 | ) 22 | 23 | func Now() int64 { 24 | return time.Now().UnixNano() 25 | } 26 | -------------------------------------------------------------------------------- /internal/timer/timer_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 | // +build linux 16 | 17 | package timer 18 | 19 | import ( 20 | "syscall" 21 | "unsafe" 22 | ) 23 | 24 | const ( 25 | // from /usr/include/linux/time.h 26 | CLOCK_MONOTONIC = 1 27 | ) 28 | 29 | // TODO(sgotti) for the moment just use a syscall so it'll work on all linux 30 | // architectures. It's slower than using a vdso but we don't have such performance 31 | // needs. Let's wait for a stdlib native monotonic clock. 32 | func Now() int64 { 33 | var ts syscall.Timespec 34 | _, _, _ = syscall.Syscall(syscall.SYS_CLOCK_GETTIME, CLOCK_MONOTONIC, uintptr(unsafe.Pointer(&ts)), 0) 35 | nsec := ts.Nano() 36 | return nsec 37 | } 38 | -------------------------------------------------------------------------------- /internal/util/k8s.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 util 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "k8s.io/client-go/tools/clientcmd" 22 | ) 23 | 24 | const ( 25 | KubePodName = "POD_NAME" 26 | 27 | KubeResourcePrefix = "stolon-cluster" 28 | 29 | KubeClusterLabel = "stolon-cluster" 30 | 31 | KubeClusterDataAnnotation = "stolon-clusterdata" 32 | KubeStatusAnnnotation = "stolon-status" 33 | ) 34 | 35 | func PodName() (string, error) { 36 | podName := os.Getenv(KubePodName) 37 | if len(podName) == 0 { 38 | return "", fmt.Errorf("missing required env variable %q", KubePodName) 39 | } 40 | return podName, nil 41 | } 42 | 43 | // NewKubeClientConfig return a kube client config that will by default use an 44 | // in cluster client config or, if not available or overridden, an external client 45 | // config using the default client behavior used also by kubectl. 46 | func NewKubeClientConfig(kubeconfigPath, context, namespace string) clientcmd.ClientConfig { 47 | rules := clientcmd.NewDefaultClientConfigLoadingRules() 48 | rules.DefaultClientConfig = &clientcmd.DefaultClientConfig 49 | 50 | if kubeconfigPath != "" { 51 | rules.ExplicitPath = kubeconfigPath 52 | } 53 | 54 | overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} 55 | 56 | if context != "" { 57 | overrides.CurrentContext = context 58 | } 59 | 60 | if namespace != "" { 61 | overrides.Context.Namespace = namespace 62 | } 63 | 64 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) 65 | } 66 | -------------------------------------------------------------------------------- /internal/util/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 util 16 | 17 | import "sort" 18 | 19 | func StringInSlice(s []string, e string) bool { 20 | for _, v := range s { 21 | if v == e { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | 28 | // CompareStringSlice compares two slices of strings, a nil slice is considered an empty one 29 | func CompareStringSlice(a []string, b []string) bool { 30 | if len(a) != len(b) { 31 | return false 32 | } 33 | 34 | for i, v := range a { 35 | if v != b[i] { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | 42 | // CompareStringSliceNoOrder compares two slices of strings regardless of their order, a nil slice is considered an empty one 43 | func CompareStringSliceNoOrder(a []string, b []string) bool { 44 | if len(a) != len(b) { 45 | return false 46 | } 47 | 48 | // This isn't the faster way but it's cleaner and enough for us 49 | 50 | // Take a copy of the original slice 51 | a = append([]string(nil), a...) 52 | b = append([]string(nil), b...) 53 | 54 | sort.Strings(a) 55 | sort.Strings(b) 56 | 57 | for i, v := range a { 58 | if v != b[i] { 59 | return false 60 | } 61 | } 62 | return true 63 | } 64 | 65 | // CommonElements return the common elements in two slices of strings 66 | func CommonElements(a []string, b []string) []string { 67 | common := []string{} 68 | for _, v := range a { 69 | if StringInSlice(b, v) { 70 | common = append(common, v) 71 | } 72 | } 73 | return common 74 | } 75 | 76 | // Difference returns elements in a - b 77 | func Difference(a []string, b []string) []string { 78 | diff := []string{} 79 | for _, v := range a { 80 | if !StringInSlice(b, v) { 81 | diff = append(diff, v) 82 | } 83 | } 84 | return diff 85 | } 86 | -------------------------------------------------------------------------------- /internal/util/slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 util 16 | 17 | import "testing" 18 | 19 | func TestCompareStringSlice(t *testing.T) { 20 | tests := []struct { 21 | a []string 22 | b []string 23 | ok bool 24 | }{ 25 | {[]string{}, []string{}, true}, 26 | {[]string{"", ""}, []string{""}, false}, 27 | {[]string{"", ""}, []string{"", ""}, true}, 28 | {[]string{"a", "b"}, []string{"a", "b"}, true}, 29 | {[]string{"a", "b"}, []string{"b", "a"}, false}, 30 | {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, 31 | {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, 32 | {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, false}, 33 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, 34 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, 35 | } 36 | 37 | for i, tt := range tests { 38 | ok := CompareStringSlice(tt.a, tt.b) 39 | if ok != tt.ok { 40 | t.Errorf("%d: got %t but wanted: %t a: %v, b: %v", i, ok, tt.ok, tt.a, tt.b) 41 | } 42 | } 43 | } 44 | 45 | func TestCompareStringSliceNoOrder(t *testing.T) { 46 | tests := []struct { 47 | a []string 48 | b []string 49 | ok bool 50 | }{ 51 | {[]string{}, []string{}, true}, 52 | {[]string{"", ""}, []string{""}, false}, 53 | {[]string{"", ""}, []string{"", ""}, true}, 54 | {[]string{"a", "b"}, []string{"a", "b"}, true}, 55 | {[]string{"a", "b"}, []string{"b", "a"}, true}, 56 | {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, 57 | {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, 58 | {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, true}, 59 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, 60 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, 61 | } 62 | 63 | for i, tt := range tests { 64 | ok := CompareStringSliceNoOrder(tt.a, tt.b) 65 | if ok != tt.ok { 66 | t.Errorf("%d: got %t but wanted: %t a: %v, b: %v", i, ok, tt.ok, tt.a, tt.b) 67 | } 68 | } 69 | } 70 | 71 | func TestDifference(t *testing.T) { 72 | tests := []struct { 73 | a []string 74 | b []string 75 | r []string 76 | }{ 77 | {[]string{}, []string{}, []string{}}, 78 | {[]string{"", ""}, []string{""}, []string{}}, 79 | {[]string{"", ""}, []string{"", ""}, []string{}}, 80 | {[]string{"", ""}, []string{"a", "", "b"}, []string{}}, 81 | {[]string{"a", "b"}, []string{"a", "b"}, []string{}}, 82 | {[]string{"a", "b"}, []string{"b", "a"}, []string{}}, 83 | {[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}}, 84 | {[]string{"a", "b", "c"}, []string{"a", "b"}, []string{"c"}}, 85 | {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{}}, 86 | {[]string{"a", "b"}, []string{"c", "a", "b"}, []string{}}, 87 | {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, []string{}}, 88 | {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, []string{}}, 89 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, []string{}}, 90 | {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, []string{}}, 91 | } 92 | 93 | for i, tt := range tests { 94 | r := Difference(tt.a, tt.b) 95 | if !CompareStringSliceNoOrder(r, tt.r) { 96 | t.Errorf("%d: got %v but wanted: %v a: %v, b: %v", i, r, tt.r, tt.a, tt.b) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/util/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sorint.lab 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 | // 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 util 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "os/user" 21 | ) 22 | 23 | func GetUser() (string, error) { 24 | u, err := user.Current() 25 | if err == nil { 26 | return u.Username, nil 27 | } 28 | 29 | name := os.Getenv("USER") 30 | if name != "" { 31 | return name, nil 32 | } 33 | 34 | return "", fmt.Errorf("cannot detect current user") 35 | } 36 | -------------------------------------------------------------------------------- /licenses/LICENSE-BSD-3-Clause: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) [year], [fullname] 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /logos/stolon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorintlab/stolon/bcea7a7548d5e2af95025cefbfd526f98c15b5e6/logos/stolon-color.png -------------------------------------------------------------------------------- /logos/stolon-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /scripts/agola-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | apk add make jq 6 | 7 | echo -n "Waiting for docker to be ready" 8 | until curl -s --fail http://127.0.0.1:10080/docker-ready; do 9 | sleep 1; 10 | echo -n "." 11 | done 12 | echo " Ready" 13 | 14 | cd /stolon 15 | 16 | make PGVERSION=11 TAG=stolon:master-pg11 docker 17 | 18 | pushd examples/kubernetes 19 | 20 | echo -n "Waiting for kubernetes to be ready" 21 | until curl -s --fail http://127.0.0.1:10080/kubernetes-ready; do 22 | sleep 1; 23 | echo -n "." 24 | done 25 | echo " Ready" 26 | 27 | sed -i 's#sorintlab/stolon:master-pg10#stolon:master-pg11#' *.yaml 28 | 29 | for i in role.yaml role-binding.yaml secret.yaml stolon-sentinel.yaml stolon-keeper.yaml stolon-proxy.yaml stolon-proxy-service.yaml ; do 30 | kubectl apply -f $i 31 | done 32 | 33 | popd 34 | 35 | KUBERUN="kubectl run --quiet -i -t stolonctl --image=stolon:master-pg11 --restart=Never --rm --" 36 | 37 | $KUBERUN /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap init -y 38 | 39 | OK=false 40 | COUNT=0 41 | while [ $COUNT -lt 120 ]; do 42 | OUT=$($KUBERUN /usr/local/bin/stolonctl --cluster-name kube-stolon --store-backend kubernetes --kube-resource-kind configmap clusterdata read | jq .cluster.status.phase) 43 | if [ "$OUT" == '"normal"' ]; then 44 | OK=true 45 | break 46 | fi 47 | 48 | COUNT=$((COUNT + 1)) 49 | sleep 1 50 | done 51 | 52 | # report some debug output 53 | kubectl get all 54 | $KUBERUN /usr/local/bin/stolonctl --cluster-name kube-stolon --store-backend kubernetes --kube-resource-kind configmap status 55 | $KUBERUN /usr/local/bin/stolonctl --cluster-name kube-stolon --store-backend kubernetes --kube-resource-kind configmap clusterdata read | jq . 56 | 57 | if [ "$OK" != "true" ]; then 58 | echo "stolon cluster not correctly setup" 59 | exit 1 60 | fi 61 | 62 | echo "stolon cluster successfully setup" 63 | -------------------------------------------------------------------------------- /scripts/build-binary: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | VER=$1 6 | PROJ="stolon" 7 | 8 | if [ -z "$1" ]; then 9 | echo "Usage: ${0} VERSION" >> /dev/stderr 10 | exit 255 11 | fi 12 | 13 | set -u 14 | 15 | function setup_env { 16 | local proj=${1} 17 | local ver=${2} 18 | 19 | if [ ! -d ${proj} ]; then 20 | git clone https://github.com/sorintlab/${proj} 21 | fi 22 | 23 | pushd ${proj} >/dev/null 24 | git checkout master 25 | git fetch --all 26 | git reset --hard origin/master 27 | git checkout $ver 28 | popd >/dev/null 29 | } 30 | 31 | 32 | function package { 33 | local target=${1} 34 | local srcdir="${2}/bin" 35 | 36 | local ccdir="${srcdir}/${GOOS}_${GOARCH}" 37 | if [ -d ${ccdir} ]; then 38 | srcdir=${ccdir} 39 | fi 40 | 41 | mkdir ${target}/bin 42 | 43 | for bin in stolon-keeper stolon-sentinel stolon-proxy stolonctl; do 44 | cp ${srcdir}/${bin} ${target}/bin 45 | done 46 | 47 | cp stolon/README.md ${target}/README.md 48 | 49 | cp -R stolon/doc ${target}/doc 50 | cp -R stolon/examples ${target}/examples 51 | rm -rf ${target}/examples/kubernetes/image/docker/bin 52 | rm -rf ${target}/examples/docker/bin 53 | } 54 | 55 | function main { 56 | mkdir -p release 57 | cd release 58 | setup_env ${PROJ} ${VER} 59 | 60 | for os in linux; do 61 | export GOOS=${os} 62 | export GOARCH="amd64" 63 | 64 | pushd stolon >/dev/null 65 | make 66 | popd >/dev/null 67 | 68 | TARGET="stolon-${VER}-${GOOS}-${GOARCH}" 69 | mkdir -p ${TARGET} 70 | package ${TARGET} ${PROJ} 71 | 72 | if [ ${GOOS} == "linux" ]; then 73 | tar cfz ${TARGET}.tar.gz ${TARGET} 74 | echo "Wrote release/${TARGET}.tar.gz" 75 | else 76 | zip -qr ${TARGET}.zip ${TARGET} 77 | echo "Wrote release/${TARGET}.zip" 78 | fi 79 | done 80 | } 81 | 82 | main 83 | -------------------------------------------------------------------------------- /scripts/gen_commands_doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Sorint.lab 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 | // 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 | "fmt" 19 | "log" 20 | "os" 21 | 22 | keepercmd "github.com/sorintlab/stolon/cmd/keeper/cmd" 23 | proxycmd "github.com/sorintlab/stolon/cmd/proxy/cmd" 24 | sentinelcmd "github.com/sorintlab/stolon/cmd/sentinel/cmd" 25 | stolonctlcmd "github.com/sorintlab/stolon/cmd/stolonctl/cmd" 26 | 27 | "github.com/spf13/cobra/doc" 28 | ) 29 | 30 | func main() { 31 | // use os.Args instead of "flags" because "flags" will mess up the man pages! 32 | var outDir string 33 | if len(os.Args) == 2 { 34 | outDir = os.Args[1] 35 | } else { 36 | fmt.Fprintf(os.Stderr, "usage: %s [output directory]", os.Args[0]) 37 | os.Exit(1) 38 | } 39 | 40 | if err := doc.GenMarkdownTree(keepercmd.CmdKeeper, outDir); err != nil { 41 | log.Fatal(err) 42 | } 43 | if err := doc.GenMarkdownTree(sentinelcmd.CmdSentinel, outDir); err != nil { 44 | log.Fatal(err) 45 | } 46 | if err := doc.GenMarkdownTree(proxycmd.CmdProxy, outDir); err != nil { 47 | log.Fatal(err) 48 | } 49 | if err := doc.GenMarkdownTree(stolonctlcmd.CmdStolonCtl, outDir); err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/gen_commands_doc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source $(dirname $0)/readlinkdashf.sh 5 | 6 | BASEDIR=$(readlinkdashf $(dirname $0)/..) 7 | 8 | go run $BASEDIR/scripts/gen_commands_doc.go $BASEDIR/doc/commands/ 9 | -------------------------------------------------------------------------------- /scripts/git-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # parse the current git commit hash 4 | COMMIT=`git rev-parse HEAD` 5 | 6 | # check if the current commit has a matching tag 7 | TAG=$(git describe --exact-match --abbrev=0 --tags ${COMMIT} 2> /dev/null || true) 8 | 9 | # use the matching tag as the version, if available 10 | if [ -z "$TAG" ]; then 11 | VERSION=$COMMIT 12 | else 13 | VERSION=$TAG 14 | fi 15 | 16 | # check for changed files (not untracked files) 17 | if [ -n "$(git diff --shortstat 2> /dev/null | tail -n1)" ]; then 18 | VERSION="${VERSION}-dirty" 19 | fi 20 | 21 | echo $VERSION 22 | -------------------------------------------------------------------------------- /scripts/readlinkdashf.sh: -------------------------------------------------------------------------------- 1 | # Cross compatibility with osx 2 | # origin source: https://github.com/kubernetes/kubernetes/ blob/master/hack/lib/init.sh#L102 3 | function readlinkdashf() { 4 | # run in a subshell for simpler 'cd' 5 | ( 6 | if [[ -d "$1" ]]; then # This also catch symlinks to dirs. 7 | cd "$1" 8 | pwd -P 9 | else 10 | cd $(dirname "$1") 11 | local f 12 | f=$(basename "$1") 13 | if [[ -L "$f" ]]; then 14 | readlink "$f" 15 | else 16 | echo "$(pwd -P)/${f}" 17 | fi 18 | fi 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Build all release binaries and sources into "./release" directory. 4 | # 5 | 6 | set -e 7 | 8 | VERSION=$1 9 | if [ -z "${VERSION}" ]; then 10 | echo "Usage: ${0} VERSION" >> /dev/stderr 11 | exit 255 12 | fi 13 | 14 | BASEDIR=$(readlink -f $(dirname $0))/.. 15 | BINDIR=${BASEDIR}/bin 16 | 17 | if [ $PWD != $BASEDIR ]; then 18 | cd $BASEDIR 19 | fi 20 | 21 | 22 | echo Building stolon binary... 23 | ./scripts/build-binary ${VERSION} 24 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Test script code thanks to coreos/etcd 4 | # 5 | # Run all tests 6 | # ./test 7 | # ./test -v 8 | # 9 | # Run also integration tests 10 | # INTEGRATION=1 STOLON_TEST_STORE_BACKEND=etcdv3 ./test 11 | # 12 | set -e 13 | 14 | source $(dirname $0)/scripts/readlinkdashf.sh 15 | BASEDIR=$(readlinkdashf $(dirname $0)) 16 | BINDIR=${BASEDIR}/bin 17 | 18 | if [ $PWD != $BASEDIR ]; then 19 | cd $BASEDIR 20 | fi 21 | 22 | ORG_PATH="github.com/sorintlab" 23 | REPO_PATH="${ORG_PATH}/stolon" 24 | 25 | # test all packages excluding integration tests 26 | IGNORE_PKGS="(vendor/|tests/integration)" 27 | PACKAGES=$(find . -name \*_test.go | while read -r a; do dirname "$a"; done | sort | uniq | grep -vE "$IGNORE_PKGS" | sed "s|\./||g") 28 | 29 | # prepend REPO_PATH to each local package 30 | split=$PACKAGES 31 | PACKAGES="" 32 | for a in $split; do PACKAGES="$PACKAGES ${REPO_PATH}/${a}"; done 33 | 34 | echo "Running tests..." 35 | 36 | COVER=${COVER:-"-cover"} 37 | 38 | ## Use this code in case we are testing with multiple go versions that have different gofmt outputs 39 | # # Use only of specific go version to run gofmt since it'll break when some formatting rules change between versions 40 | # GOFMT_VERSION="go1.14" 41 | # MAJOR_GOVERSION=$( echo -n $(go version) | grep -o 'go1\.[0-9]*' || true ) 42 | # if [ "${MAJOR_GOVERSION}" == "${GOFMT_VERSION}" ]; then 43 | # echo "Checking gofmt..." 44 | # fmtRes=$(gofmt -l $(find . -type f -name '*.go' ! -path './vendor/*' ! -path '*/\.*')) 45 | # if [ -n "${fmtRes}" ]; then 46 | # echo -e "gofmt checking failed:\n${fmtRes}" 47 | # exit 255 48 | # fi 49 | # fi 50 | 51 | echo "Checking gofmt..." 52 | fmtRes=$(gofmt -l $(find . -type f -name '*.go' ! -path './vendor/*' ! -path '*/\.*')) 53 | if [ -n "${fmtRes}" ]; then 54 | echo -e "gofmt checking failed:\n${fmtRes}" 55 | exit 255 56 | fi 57 | 58 | echo "Checking govet..." 59 | vetRes=$(go vet ${PACKAGES}) 60 | if [ -n "${vetRes}" ]; then 61 | echo -e "govet checking failed:\n${vetRes}" 62 | exit 255 63 | fi 64 | 65 | echo "Checking govet -shadow ..." 66 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow 67 | export PATH="$(go env GOPATH)/bin":${PATH} 68 | shadow_tool=$(which shadow) 69 | vetRes=$(${shadow_tool} ${PACKAGES}) 70 | if [ -n "${vetRes}" ]; then 71 | echo -e "govet checking ${path} failed:\n${vetRes}" 72 | exit 255 73 | fi 74 | 75 | echo "Checking for license header..." 76 | licRes=$(for file in $(find . -type f -iname '*.go' ! -path './vendor/*'); do 77 | head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}" 78 | done;) 79 | if [ -n "${licRes}" ]; then 80 | echo -e "license header checking failed:\n${licRes}" 81 | exit 255 82 | fi 83 | 84 | 85 | echo "Checking with golangci-lint" 86 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b ${BINDIR} v1.23.6 87 | ${BINDIR}/golangci-lint run --deadline 5m 88 | 89 | echo "Running tests" 90 | go test -timeout 3m ${COVER} $@ ${PACKAGES} ${RACE} 91 | 92 | if [ -n "$INTEGRATION" ]; then 93 | echo "Running integration tests..." 94 | if [ -z ${STOLON_TEST_STORE_BACKEND} ]; then 95 | echo "STOLON_TEST_STORE_BACKEND env var needs to be defined (etcd or consul)" 96 | exit 1 97 | fi 98 | export STKEEPER_BIN=${BINDIR}/stolon-keeper 99 | export STSENTINEL_BIN=${BINDIR}/stolon-sentinel 100 | export STPROXY_BIN=${BINDIR}/stolon-proxy 101 | export STCTL_BIN=${BINDIR}/stolonctl 102 | if [ "${STOLON_TEST_STORE_BACKEND}" == "etcd" -o "${STOLON_TEST_STORE_BACKEND}" == "etcdv2" -o "${STOLON_TEST_STORE_BACKEND}" == "etcdv3" ]; then 103 | if [ -z ${ETCD_BIN} ]; then 104 | if [ -z $(which etcd) ]; then 105 | echo "cannot find etcd in PATH and ETCD_BIN environment variable not defined" 106 | exit 1 107 | fi 108 | ETCD_BIN=$(which etcd) 109 | fi 110 | echo "using etcd from $ETCD_BIN" 111 | export ETCD_BIN 112 | elif [ "${STOLON_TEST_STORE_BACKEND}" == "consul" ]; then 113 | if [ -z ${CONSUL_BIN} ]; then 114 | if [ -z $(which consul) ]; then 115 | echo "cannot find consul in PATH and CONSUL_BIN environment variable not defined" 116 | exit 1 117 | fi 118 | CONSUL_BIN=$(which consul) 119 | fi 120 | echo "using consul from $CONSUL_BIN" 121 | export CONSUL_BIN 122 | else 123 | echo "Unknown store backend: \"${STOLON_TEST_STORE_BACKEND}\"" 124 | exit 1 125 | fi 126 | 127 | [ -z "$PARALLEL" ] && PARALLEL=4 128 | go test -timeout 20m $@ -v -count 1 -parallel ${PARALLEL} ${REPO_PATH}/tests/integration 129 | fi 130 | 131 | echo "Success" 132 | --------------------------------------------------------------------------------