pax_global_header00006660000000000000000000000064150215425240014512gustar00rootroot0000000000000052 comment=7d8ca2c5a7fd58540725cddb1a3fb1261081f03a vip-manager-4.0.0/000077500000000000000000000000001502154252400137215ustar00rootroot00000000000000vip-manager-4.0.0/.github/000077500000000000000000000000001502154252400152615ustar00rootroot00000000000000vip-manager-4.0.0/.github/dependabot.yml000066400000000000000000000005301502154252400201070ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for Go modules - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" vip-manager-4.0.0/.github/workflows/000077500000000000000000000000001502154252400173165ustar00rootroot00000000000000vip-manager-4.0.0/.github/workflows/build.yml000066400000000000000000000027231502154252400211440ustar00rootroot00000000000000name: Go Build & Test on: pull_request: workflow_dispatch: jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] name: Build & Test steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Golang uses: actions/setup-go@v5 with: go-version: '1.23' - name: Install etcd if: runner.os == 'Linux' run: | curl -L https://github.com/etcd-io/etcd/releases/download/v3.5.18/etcd-v3.5.18-linux-amd64.tar.gz -o etcd-v3.5.18-linux-amd64.tar.gz tar xzvf etcd-v3.5.18-linux-amd64.tar.gz sudo mv etcd-v3.5.18-linux-amd64/etcd /usr/local/bin/ sudo mv etcd-v3.5.18-linux-amd64/etcdctl /usr/local/bin/ etcd --version sudo apt install -y ncat - name: Get dependencies run: | go mod download go version go generate ./... go build - name: GolangCI-Lint if: runner.os == 'Linux' uses: golangci/golangci-lint-action@v6 with: version: latest args: --verbose - name: Test E2E if: runner.os == 'Linux' run: | sudo test/behaviour_test.sh sudo rm -r default.etcd || true - name: Run GoReleaser if: runner.os == 'Linux' uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --snapshot --skip=publish --clean vip-manager-4.0.0/.github/workflows/codeql-analysis.yml000066400000000000000000000014371502154252400231360ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '19 11 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Build run: | go mod download go version go build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3vip-manager-4.0.0/.github/workflows/release.yml000066400000000000000000000011321502154252400214560ustar00rootroot00000000000000name: Release on: release: types: [created] jobs: goreleaser: if: true # false to skip job during debug runs-on: ubuntu-latest name: goreleaser steps: - name: Set up Golang uses: actions/setup-go@v5 with: go-version: '1.23' - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Unshallow run: git fetch --prune --unshallow - name: Release via goreleaser uses: goreleaser/goreleaser-action@v6 with: args: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}vip-manager-4.0.0/.github/workflows/stale.yml000066400000000000000000000020201502154252400211430ustar00rootroot00000000000000name: Close Stale Issues and PRs on: schedule: - cron: '0 0 * * *' workflow_dispatch: jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-label: 'stale' stale-pr-label: 'stale' stale-issue-message: | 📅 This issue has been automatically marked as stale because lack of recent activity. It will be closed if no further activity occurs. ♻️ If you think there is new information allowing us to address the issue, please reopen it and provide us with updated details. 🤝 Thank you for your contributions. stale-pr-message: | 📅 This PR has been automatically marked as stale because lack of recent activity. It will be closed if no further activity occurs. ♻️ If you think there is new information allowing us to address this PR, please reopen it and provide us with updated details. 🤝 Thank you for your contributions.vip-manager-4.0.0/.gitignore000066400000000000000000000000401502154252400157030ustar00rootroot00000000000000*.deb *.rpm *.exe tmp/ .vscode/ vip-manager-4.0.0/.golangci.yml000066400000000000000000000003221502154252400163020ustar00rootroot00000000000000linters: enable: - gocyclo - revive - misspell - unused linters-settings: gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 16vip-manager-4.0.0/.goreleaser.yml000066400000000000000000000031151502154252400166520ustar00rootroot00000000000000version: 2 before: hooks: - go mod tidy builds: - env: - CGO_ENABLED=0 goos: - linux - windows hooks: pre: go generate ./... archives: - name_template: >- {{ .ProjectName }}_{{ .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} wrap_in_directory: true format_overrides: - goos: windows formats: ['zip'] files: - LICENSE - README.md - src: 'vipconfig/*.yml' strip_parent: true checksum: name_template: 'checksums.txt' changelog: sort: asc filters: exclude: - '^docs:' - '^test:' nfpms: # note that this is an array of nfpm configs - file_name_template: >- {{ .ProjectName }}_{{ .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} vendor: CYBERTEC PostgreSQL International GmbH homepage: https://github.com/cybertec-postgresql/vip-manager/ maintainer: Julian Markwort description: Manages a virtual IP based on state kept in etcd/consul license: BSD 2-Clause License section: default provides: - vip-manager formats: - deb - rpm contents: - src: vipconfig/vip-manager.yml dst: /etc/default/vip-manager.yml type: "config|noreplace" - src: vip-manager.service dst: /lib/systemd/system/vip-manager.service - src: LICENSE dst: /usr/share/doc/vip-manager/LICENSE vip-manager-4.0.0/LICENSE000066400000000000000000000025011502154252400147240ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2017, "Cybertec PostgreSQL International GmbH" All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. vip-manager-4.0.0/Makefile000066400000000000000000000011121502154252400153540ustar00rootroot00000000000000GOENV=CGO_ENABLED=0 all: vip-manager vip-manager: *.go */*.go $(GOENV) go build -ldflags="-s -w -X main.version=`git describe --tags --abbrev=0` -X main.commit=`git show -s --format=%H HEAD` -X main.date=`git show -s --format=%cI HEAD`" . install: install -d $(DESTDIR)/usr/bin install vip-manager $(DESTDIR)/usr/bin/vip-manager install -d $(DESTDIR)/etc/default install vipconfig/vip-manager.yml $(DESTDIR)/etc/default/vip-manager.yml DESTDIR=tmp package: goreleaser release --snapshot --skip-publish --rm-dist clean: $(RM) vip-manager $(RM) -r dist $(RM) -r $(DESTDIR)vip-manager-4.0.0/README.md000066400000000000000000000265301502154252400152060ustar00rootroot00000000000000[![License: MIT](https://img.shields.io/badge/License-BSD-green.svg)](https://opensource.org/licenses/BSD-2) ![Build&Test](https://github.com/cybertec-postgresql/vip-manager/workflows/Go%20Build%20&%20Test/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/cybertec-postgresql/vip-manager)](https://goreportcard.com/report/github.com/cybertec-postgresql/vip-manager) [![Release](https://img.shields.io/github/release/cybertec-postgresql/vip-manager.svg?include_prereleases)](https://github.com/cybertec-postgresql/vip-manager/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/cybertec-postgresql/vip-manager/total?style=flat-square)](https://github.com/cybertec-postgresql/vip-manager/releases) # vip-manager Manages a virtual IP based on state kept in `etcd`, `Consul` or using `Patroni` REST API ## Table of Contents - [Prerequisites](#prerequisites) - [Building](#building) - [Installing from package](#installing-from-package) - [Installing from source](#installing-from-source) - [Environment prerequisites](#environment-prerequisites) - [PostgreSQL prerequisites](#postgresql-prerequisites) - [Configuration](#configuration) - [Configuration - Hetzner](#configuration---hetzner) - [Credential File - Hetzmer](#credential-file---hetzner) - [Debugging](#debugging) - [Author](#author) ## Prerequisites - `go` >= 1.19 - `make` (optional) - `goreleaser` (optional) ## Building 1. clone this repo ```shell git clone https://github.com/cybertec-postgresql/vip-manager.git ``` 1. Build the binary using `make` or `go build`. 1. To build your own packages (.deb, .rpm, .zip, etc.), run ```shell make package ``` or ```shell goreleaser release --snapshot --skip-publish --rm-dist ``` ## Installing from package You can download .rpm or .deb packages here, on the [Releases](https://github.com/cybertec-postgresql/vip-manager/releases) page. On Debian and Ubuntu, the universe repositories should provide you with vip-manager, though the version may be not as recent. > [!IMPORTANT] Our packages are probably not compatible with the one from those repositories, do not try to install them side-by-side. ## Installing from source - Follow the steps to [build](#building) vip-manager. - Run `DESTDIR=/tmp make install` to copy the binary, service files and config file into the destination of your choice. - Edit config to your needs, then run `systemctl daemon-reload`, then `systemctl start vip-manager`. > [!NOTE] systemd will only pick the service files up if you chose a `DESTDIR` so that it can find it. Usually `DESTDIR=''` should work. ## Environment prerequisites When vip-manager is in charge of registering and deregistering the VIP locally, it needs superuser privileges to do so. This is not required when vip-manager is used to manage a VIP through some API, e.g. Hetzner Robot API or Hetzner Cloud API. > [!NOTE] > At some point it would be great to reduce this requirement to only the `CAP_NET_RAW` and `CAP_NET_ADMIN` capabilities, which could be added by a superuser to the vip-manager binary _once_. > Right now, this is not possible since vip-manager launches plain shell commands to register and deregister virtual IP addresses locally (at least on linux), so the whole user would need these privileges. > When vip-manager is eventually taught to directly use a library that directly uses the Linux kernel's API to register/deregister the VIP, the capabilities set for the binary will suffice. ## PostgreSQL prerequisites For any virtual IP based solutions to work in general with Postgres you need to make sure that it is configured to automatically scan and bind to all found network interfaces. So something like `*` or `0.0.0.0` (IPv4 only) is needed for the `listen_addresses` parameter to activate the automatic binding. This again might not be suitable for all use cases where security is paramount for example. ### nonlocal bind If you can't set `listen_addresses` to a wildcard address, you can explicitly specify only those adresses that you want to listen to. However, if you add the virtual IP to those addresses, PostgreSQL will fail to start when that address is not yet registered on one of the interfaces of the machine. You need to configure the kernel to allow "nonlocal bind" of IP (v4) addresses: - temporarily: ```bash sysctl -w net.ipv4.ip_nonlocal_bind=1 ``` - permanently: ```bash echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf sysctl -p ``` ## Configuration The configuration can be passed to the executable through argument flags, environment variables or through a YAML config file. Run `vip-manager --help` to see the available flags. > [!NOTE] > The location of the YAML config file can be specified with the --config flag. > An exemplary config file is installed into `/etc/default/vip-manager.yml` or is available in the vipconfig directory in the repository of the software. Configuration is now (from release v1.0 on) handled using the [`viper`](https://github.com/spf13/viper) library. This means that environment variables, command line flags, and config files can be used to configure vip-manager. When using different configuration sources simultaneously, this is the precedence order: - flag - env - config > [!NOTE] > So flags always overwrite env variables and entries from the config file. Env variables overwrite the config file entries. All flags and file entries are written in lower case. To make longer multi-word flags and entries readable, they are separated by dashes, e.g. `retry-num`. If you put a flag or file entry into uppercase and replace dashes with underscores, you end up with the format of environment variables. To avoid overlapping configuration with other applications, the env variables are additionall prefixed with `VIP_`, e.g. `VIP_RETRY_NUM`. This is a list of all avaiable configuration items: | flag/yaml key | env notation | required | example | description | | ----------------- | --------------------- | --------- | --------------------------- | ----------- | | `ip` | `VIP_IP` | yes | `10.10.10.123` | The virtual IP address that will be managed. | | `netmask` | `VIP_NETMASK` | yes | `24` | The netmask that is associated with the subnet that the virtual IP `vip` is part of. | | `interface` | `VIP_INTERFACE` | yes | `eth0` | A local network interface on the machine that runs vip-manager. Required when using `manager-type=basic`. The vip will be added to and removed from this interface. | | `trigger-key` | `VIP_TRIGGER_KEY` | yes | `/service/pgcluster/leader` | The key in the DCS or the Patroni REST endpoint (e.g. `/leader`) that will be monitored by vip-manager. Must match `//leader` from Patroni config. When the value returned by the DCS equals `trigger-value`, vip-manager will make sure that the virtual IP is registered to this machine. If it does not match, vip-manager makes sure that the virtual IP is not registered to this machine. | | `trigger-value` | `VIP_TRIGGER_VALUE` | no | `pgcluster_member_1` | The value that the DCS' answer for `trigger-key` will be matched to. Must match `` from Patroni config for DCS or the HTTP response for Patroni REST API. This is usually set to the name of the Patroni cluster member that this vip-manager instance is associated with. Defaults to the machine's hostname or to 200 for Patroni. | | `manager-type` | `VIP_MANAGER_TYPE` | no | `basic` | Either `basic` or `hetzner`. This describes the mechanism that is used to manage the virtual IP. Defaults to `basic`. | | `dcs-type` | `VIP_DCS_TYPE` | no | `etcd` | The type of DCS that vip-manager will use to monitor the `trigger-key`. Defaults to `etcd`. | | `dcs-endpoints` | `VIP_DCS_ENDPOINTS` | no | `http://10.10.11.1:2379` | A url that defines where to reach the DCS or Patroni REST API. Multiple endpoints can be passed to the flag or env variable using a comma-separated-list. In the config file, a list can be specified, see the sample config for an example. Defaults to `http://127.0.0.1:2379` for `dcs-type=etcd`, `http://127.0.0.1:8500` for `dcs-type=consul` and `http://127.0.0.1:8008` for `dcs-type=patroni`. | | `etcd-user` | `VIP_ETCD_USER` | no | `patroni` | A username that is allowed to look at the `trigger-key` in an etcd DCS. Optional when using `dcs-type=etcd` . | | `etcd-password` | `VIP_ETCD_PASSWORD` | no | `snakeoil` | The password for `etcd-user`. Optional when using `dcs-type=etcd` . Requires that `etcd-user` is also set. | | `consul-token` | `VIP_CONSUL_TOKEN` | no | `snakeoil` | A token that can be used with the consul-API for authentication. Optional when using `dcs-type=consul` . | | `interval` | `VIP_INTERVAL` | no | `1000` | The time vip-manager main loop sleeps before checking for changes. Measured in ms. Defaults to `1000`. Doesn't affect etcd checker since v2.3.0. | | `retry-after` | `VIP_RETRY_AFTER` | no | `250` | The time to wait before retrying interactions with components outside of vip-manager. Measured in ms. Defaults to `250`. | | `retry-num` | `VIP_RETRY_NUM` | no | `3` | The number of times interactions with components outside of vip-manager are retried. Defaults to `3`. | | `etcd-ca-le` | `VIP_ETCD_CA_FILE` | no | `/etc/etcd/ca.cert.pem` | A certificate authority file that can be used to verify the certificate provided by etcd endpoints. Make sure to change `dcs-endpoints` to reflect that `https` is used. | | `etcd-cert-le` | `VIP_ETCD_CERT_FILE` | no | `/etc/etcd/client.cert.pem` | A client certificate that is used to authenticate against etcd endpoints. Requires `etcd-ca-file` to be set as well. | | `etcd-key-le` | `VIP_ETCD_KEY_FILE` | no | `/etc/etcd/client.key.pem` | A private key for the client certificate, used to decrypt messages sent by etcd endpoints. Required when `etcd-cert-file` is specified. | | `verbose` | `VIP_VERBOSE` | no | `true` | Enable more verbose logging. Currently only the manager-type=hetzner provides additional logs. | ## Configuration - Patroni REST API To directly use the Patroni REST API, simply set `dcs-type` to `patroni` and `trigger-key` to `/leader`. The defaults for `dcs-endpoints` (`http://127.0.0.1:8008`) and `trigger-value` (200) for the Patroni checker should work in most cases. ## Configuration - Hetzner To use vip-manager with Hetzner Robot API you need a Credential file, set `hosting_type` to `hetzner` in `/etc/default/vip-manager.yml` and your Floating-IP must be added on all Servers. The Floating-IP (VIP) will not be added or removed on the current Master node interface, Hetzner will route it to the current one. ### Credential File - Hetzner Add the File `/etc/hetzner` with your Username and Password ```shell user="myUsername" pass="myPassword" ``` ## Debugging Either: - run `vip-manager` with `--verbose` flag or - set `verbose` to `true` in `/etc/default/vip-manager.yml` - set `VIP_VERBOSE=true` > [!NOTE] > Currently only supported for `hetzner` ## Author CYBERTEC PostgreSQL International GmbH, vip-manager-4.0.0/checker/000077500000000000000000000000001502154252400153255ustar00rootroot00000000000000vip-manager-4.0.0/checker/consul_leader_checker.go000066400000000000000000000035051502154252400221620ustar00rootroot00000000000000package checker import ( "cmp" "context" "fmt" "net/url" "time" "github.com/cybertec-postgresql/vip-manager/vipconfig" "github.com/hashicorp/consul/api" ) // ConsulLeaderChecker is used to check state of the leader key in Consul type ConsulLeaderChecker struct { *vipconfig.Config *api.Client } // NewConsulLeaderChecker returns a new instance func NewConsulLeaderChecker(con *vipconfig.Config) (lc *ConsulLeaderChecker, err error) { lc = &ConsulLeaderChecker{Config: con} url, err := url.Parse(con.Endpoints[0]) if err != nil { return nil, err } config := &api.Config{ Address: fmt.Sprintf("%s:%s", url.Hostname(), url.Port()), Scheme: url.Scheme, WaitTime: time.Second, Token: cmp.Or(con.ConsulToken, ""), } if lc.Client, err = api.NewClient(config); err != nil { return nil, err } return lc, nil } // GetChangeNotificationStream checks the status in the loop func (c *ConsulLeaderChecker) GetChangeNotificationStream(ctx context.Context, out chan<- bool) error { kv := c.Client.KV() queryOptions := &api.QueryOptions{ RequireConsistent: true, } checkLoop: for { resp, _, err := kv.Get(c.TriggerKey, queryOptions) if err != nil { if ctx.Err() != nil { break checkLoop } c.Logger.Sugar().Error("consul error: ", err) out <- false time.Sleep(time.Duration(c.Interval) * time.Millisecond) continue } if resp == nil { c.Logger.Sugar().Errorf("Cannot get variable for key %s. Will try again in a second.", c.TriggerKey) out <- false time.Sleep(time.Duration(c.Interval) * time.Millisecond) continue } state := string(resp.Value) == c.TriggerValue queryOptions.WaitIndex = resp.ModifyIndex select { case <-ctx.Done(): break checkLoop case out <- state: time.Sleep(time.Duration(c.Interval) * time.Millisecond) continue } } return ctx.Err() } vip-manager-4.0.0/checker/etcd_leader_checker.go000066400000000000000000000066001502154252400215750ustar00rootroot00000000000000package checker import ( "context" "crypto/tls" "crypto/x509" "fmt" "os" "time" "github.com/cybertec-postgresql/vip-manager/vipconfig" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" ) // EtcdLeaderChecker is used to check state of the leader key in Etcd type EtcdLeaderChecker struct { *vipconfig.Config *clientv3.Client } // NewEtcdLeaderChecker returns a new instance func NewEtcdLeaderChecker(conf *vipconfig.Config) (*EtcdLeaderChecker, error) { tlsConfig, err := getTransport(conf) if err != nil { return nil, err } cfg := clientv3.Config{ Endpoints: conf.Endpoints, TLS: tlsConfig, DialKeepAliveTimeout: time.Second, DialKeepAliveTime: time.Second, Username: conf.EtcdUser, Password: conf.EtcdPassword, Logger: conf.Logger, } c, err := clientv3.New(cfg) return &EtcdLeaderChecker{conf, c}, err } func getTransport(conf *vipconfig.Config) (*tls.Config, error) { var caCertPool *x509.CertPool // create valid CertPool only if the ca certificate file exists if conf.EtcdCAFile != "" { caCert, err := os.ReadFile(conf.EtcdCAFile) if err != nil { return nil, fmt.Errorf("cannot load CA file: %s", err) } caCertPool = x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) } var certificates []tls.Certificate // create valid []Certificate only if the client cert and key files exists if conf.EtcdCertFile != "" && conf.EtcdKeyFile != "" { cert, err := tls.LoadX509KeyPair(conf.EtcdCertFile, conf.EtcdKeyFile) if err != nil { return nil, fmt.Errorf("cannot load client cert or key file: %s", err) } certificates = []tls.Certificate{cert} } tlsClientConfig := new(tls.Config) if caCertPool != nil { tlsClientConfig.RootCAs = caCertPool if certificates != nil { tlsClientConfig.Certificates = certificates } } return tlsClientConfig, nil } // get gets the current leader from etcd func (elc *EtcdLeaderChecker) get(ctx context.Context, out chan<- bool) { resp, err := elc.Get(ctx, elc.TriggerKey) if err != nil { elc.Logger.Error("Failed to get etcd value:", zap.Error(err)) out <- false return } for _, kv := range resp.Kvs { elc.Logger.Sugar().Info("Current leader from DCS:", string(kv.Value)) out <- string(kv.Value) == elc.TriggerValue } } // watch monitors the leader change from etcd func (elc *EtcdLeaderChecker) watch(ctx context.Context, out chan<- bool) error { elc.Logger.Sugar().Info("Setting WATCH on ", elc.TriggerKey) watchChan := elc.Watch(ctx, elc.TriggerKey) for { select { case <-ctx.Done(): return ctx.Err() case watchResp := <-watchChan: if watchResp.Canceled { watchChan = elc.Watch(ctx, elc.TriggerKey) elc.Logger.Sugar().Info("Resetting cancelled WATCH on ", elc.TriggerKey) continue } if err := watchResp.Err(); err != nil { elc.get(ctx, out) // RPC failed, try to get the key directly to be on the safe side continue } for _, event := range watchResp.Events { out <- string(event.Kv.Value) == elc.TriggerValue elc.Logger.Sugar().Info("Current leader from DCS: ", string(event.Kv.Value)) } } } } // GetChangeNotificationStream monitors the leader in etcd func (elc *EtcdLeaderChecker) GetChangeNotificationStream(ctx context.Context, out chan<- bool) error { defer elc.Close() go elc.get(ctx, out) wctx, cancel := context.WithCancel(ctx) defer cancel() return elc.watch(wctx, out) } vip-manager-4.0.0/checker/leader_checker.go000066400000000000000000000015711502154252400206000ustar00rootroot00000000000000package checker import ( "context" "errors" "github.com/cybertec-postgresql/vip-manager/vipconfig" ) // ErrUnsupportedEndpointType is returned for an unsupported endpoint var ErrUnsupportedEndpointType = errors.New("given endpoint type not supported") // LeaderChecker is the interface for checking leadership type LeaderChecker interface { GetChangeNotificationStream(ctx context.Context, out chan<- bool) error } // NewLeaderChecker returns a new LeaderChecker instance depending on the configuration func NewLeaderChecker(con *vipconfig.Config) (LeaderChecker, error) { var lc LeaderChecker var err error switch con.EndpointType { case "consul": lc, err = NewConsulLeaderChecker(con) case "etcd", "etcd3": lc, err = NewEtcdLeaderChecker(con) case "patroni": lc, err = NewPatroniLeaderChecker(con) default: err = ErrUnsupportedEndpointType } return lc, err } vip-manager-4.0.0/checker/patroni_leader_checker.go000066400000000000000000000025611502154252400223340ustar00rootroot00000000000000package checker import ( "context" "strconv" "time" "net/http" "github.com/cybertec-postgresql/vip-manager/vipconfig" ) // PatroniLeaderChecker will use Patroni REST API to check the leader. // --trigger-key is used to specify the endpoint to check, e.g. /leader. // --trigger-value is used to specify the HTTP code to expect, e.g. 200. type PatroniLeaderChecker struct { *vipconfig.Config *http.Client } // NewPatroniLeaderChecker returns a new instance func NewPatroniLeaderChecker(conf *vipconfig.Config) (*PatroniLeaderChecker, error) { tlsConfig, err := getTransport(conf) if err != nil { return nil, err } transport := &http.Transport{ TLSClientConfig: tlsConfig, } client := &http.Client{ Transport: transport, Timeout: time.Second, } return &PatroniLeaderChecker{ Config: conf, Client: client, }, nil } // GetChangeNotificationStream checks the status in the loop func (c *PatroniLeaderChecker) GetChangeNotificationStream(ctx context.Context, out chan<- bool) error { for { select { case <-ctx.Done(): return nil case <-time.After(time.Duration(c.Interval) * time.Millisecond): r, err := c.Client.Get(c.Endpoints[0] + c.TriggerKey) if err != nil { c.Logger.Sugar().Error("patroni REST API error:", err) continue } r.Body.Close() //throw away the body out <- strconv.Itoa(r.StatusCode) == c.TriggerValue } } } vip-manager-4.0.0/go.mod000066400000000000000000000044741502154252400150400ustar00rootroot00000000000000module github.com/cybertec-postgresql/vip-manager go 1.23.8 toolchain go1.24.1 require ( github.com/google/gopacket v1.1.19 github.com/hashicorp/consul/api v1.32.1 github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.20.1 go.etcd.io/etcd/client/v3 v3.6.1 go.uber.org/zap v1.27.0 golang.org/x/sys v0.33.0 ) require ( github.com/armon/go-metrics v0.5.3 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.etcd.io/etcd/api/v3 v3.6.1 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/armon/go-metrics => github.com/hashicorp/go-metrics v0.5.3 vip-manager-4.0.0/go.sum000066400000000000000000000743761502154252400150750ustar00rootroot00000000000000github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-metrics v0.5.3 h1:M5uADWMOGCTUNU1YuC4hfknOeHNaX54LDm4oYSucoNE= github.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/etcd/api/v3 v3.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo= go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto= go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk= go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc= go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0= go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= vip-manager-4.0.0/iphlpapi/000077500000000000000000000000001502154252400155275ustar00rootroot00000000000000vip-manager-4.0.0/iphlpapi/iphlpapi_windows.go000066400000000000000000000003711502154252400214370ustar00rootroot00000000000000package iphlpapi //sys AddIPAddress(Address uint32, IpMask uint32, IfIndex uint32, NTEContext *uint32, NTEInstance *uint32) (errcode error) = iphlpapi.AddIPAddress //sys DeleteIPAddress(NTEContext uint32) (errcode error) = iphlpapi.DeleteIPAddress vip-manager-4.0.0/iphlpapi/mksyscall_windows.go000066400000000000000000000002451502154252400216330ustar00rootroot00000000000000//go:build generate // +build generate package iphlpapi //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output ziphlapi_windows.go iphlpapi_windows.go vip-manager-4.0.0/iphlpapi/ziphlapi_windows.go000066400000000000000000000027531502154252400214570ustar00rootroot00000000000000// Code generated by 'go generate'; DO NOT EDIT. package iphlpapi import ( "syscall" "unsafe" "golang.org/x/sys/windows" ) var _ unsafe.Pointer // Do the interface allocations only once for common // Errno values. const ( errnoERROR_IO_PENDING = 997 ) var ( errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) errERROR_EINVAL error = syscall.EINVAL ) // errnoErr returns common boxed Errno values, to prevent // allocations at runtime. func errnoErr(e syscall.Errno) error { switch e { case 0: return errERROR_EINVAL case errnoERROR_IO_PENDING: return errERROR_IO_PENDING } // TODO: add more here, after collecting data on the common // error values see on Windows. (perhaps when running // all.bat?) return e } var ( modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") procAddIPAddress = modiphlpapi.NewProc("AddIPAddress") procDeleteIPAddress = modiphlpapi.NewProc("DeleteIPAddress") ) func AddIPAddress(Address uint32, IpMask uint32, IfIndex uint32, NTEContext *uint32, NTEInstance *uint32) (errcode error) { r0, _, _ := syscall.Syscall6(procAddIPAddress.Addr(), 5, uintptr(Address), uintptr(IpMask), uintptr(IfIndex), uintptr(unsafe.Pointer(NTEContext)), uintptr(unsafe.Pointer(NTEInstance)), 0) if r0 != 0 { errcode = syscall.Errno(r0) } return } func DeleteIPAddress(NTEContext uint32) (errcode error) { r0, _, _ := syscall.Syscall(procDeleteIPAddress.Addr(), 1, uintptr(NTEContext), 0, 0) if r0 != 0 { errcode = syscall.Errno(r0) } return } vip-manager-4.0.0/ipmanager/000077500000000000000000000000001502154252400156645ustar00rootroot00000000000000vip-manager-4.0.0/ipmanager/basicConfigurer.go000066400000000000000000000051431502154252400213230ustar00rootroot00000000000000package ipmanager import ( "errors" "net" "strings" "github.com/google/gopacket" "github.com/google/gopacket/layers" ) // BasicConfigurer can be used to enable vip-management on nodes // that handle their own network connection, in setups where it is // sufficient to add the virtual ip using `ip addr add ...` . // After adding the virtual ip to the specified interface, // a gratuitous ARP package is sent out to update the tables of // nearby routers and other devices. type BasicConfigurer struct { *IPConfiguration ntecontext uint32 //used by Windows to delete IP address } func newBasicConfigurer(config *IPConfiguration) (*BasicConfigurer, error) { c := &BasicConfigurer{IPConfiguration: config, ntecontext: 0} if c.Iface.HardwareAddr == nil || c.Iface.HardwareAddr.String() == "00:00:00:00:00:00" { return nil, errors.New(`Cannot run vip-manager on the loopback device as its hardware address is the local address (00:00:00:00:00:00), which prohibits sending of gratuitous ARP messages`) } return c, nil } // queryAddress returns if the address is assigned func (c *BasicConfigurer) queryAddress() bool { iface, err := net.InterfaceByName(c.Iface.Name) if err != nil { return false } addresses, err := iface.Addrs() if err != nil { return false } for _, address := range addresses { if strings.Contains(address.String(), c.getCIDR()) { return true } } return false } const ( MACAddressSize = 6 IPv4AddressSize = 4 ) // createGratuitousARP prepares a packet with a gratuitous ARP request func (c *BasicConfigurer) createGratuitousARP() ([]byte, error) { // Create the Ethernet layer ethLayer := &layers.Ethernet{ SrcMAC: c.Iface.HardwareAddr, DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // Broadcast EthernetType: layers.EthernetTypeARP, } // Create the ARP layer arpLayer := &layers.ARP{ AddrType: layers.LinkTypeEthernet, Protocol: layers.EthernetTypeIPv4, HwAddressSize: MACAddressSize, ProtAddressSize: IPv4AddressSize, Operation: layers.ARPReply, // Gratuitous ARP is sent as a reply SourceHwAddress: c.Iface.HardwareAddr, SourceProtAddress: c.IPConfiguration.VIP.AsSlice(), DstHwAddress: c.Iface.HardwareAddr, // Gratuitous ARP targets itself DstProtAddress: c.IPConfiguration.VIP.AsSlice(), } // Create a packet with the layers buffer := gopacket.NewSerializeBuffer() opts := gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, } if err := gopacket.SerializeLayers(buffer, opts, ethLayer, arpLayer); err != nil { return nil, err } return buffer.Bytes(), nil } vip-manager-4.0.0/ipmanager/basicConfigurer_linux.go000066400000000000000000000034601502154252400225420ustar00rootroot00000000000000package ipmanager import ( "net" "os/exec" "syscall" ) // htons converts uint16 to network byte order func htons(i uint16) uint16 { return (i<<8)&0xff00 | i>>8 } func sendPacketLinux(iface net.Interface, packetData []byte) error { fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL))) if err != nil { return err } defer syscall.Close(fd) var sll syscall.SockaddrLinklayer sll.Protocol = htons(syscall.ETH_P_ARP) sll.Ifindex = iface.Index sll.Hatype = syscall.ARPHRD_ETHER sll.Pkttype = syscall.PACKET_BROADCAST if err = syscall.Bind(fd, &sll); err != nil { return err } return syscall.Sendto(fd, packetData, 0, &sll) } // configureAddress assigns virtual IP address func (c *BasicConfigurer) configureAddress() bool { log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name) result := c.runAddressConfiguration("add") if result { if buff, err := c.createGratuitousARP(); err != nil { log.Warn("Failed to compose gratuitous ARP request: ", err) } else { if err := sendPacketLinux(c.Iface, buff); err != nil { log.Warn("Failed to send gratuitous ARP request: ", err) } } } return result } // deconfigureAddress drops virtual IP address func (c *BasicConfigurer) deconfigureAddress() bool { log.Infof("Removing address %s on %s", c.getCIDR(), c.Iface.Name) return c.runAddressConfiguration("delete") } func (c *BasicConfigurer) runAddressConfiguration(action string) bool { cmd := exec.Command("ip", "addr", action, c.getCIDR(), "dev", c.Iface.Name) output, err := cmd.CombinedOutput() switch err.(type) { case *exec.ExitError: log.Infof("Got error %s", output) return false } if err != nil { log.Infof("Error running ip address %s %s on %s: %s", action, c.VIP, c.Iface.Name, err) return false } return true } vip-manager-4.0.0/ipmanager/basicConfigurer_windows.go000066400000000000000000000031311502154252400230700ustar00rootroot00000000000000package ipmanager import ( "encoding/binary" "net" "github.com/cybertec-postgresql/vip-manager/iphlpapi" ) func sendPacketWindows(iface net.Interface, packetData []byte) error { // Open a raw socket using Winsock conn, err := net.Dial("ip4:ethernet", iface.HardwareAddr.String()) if err != nil { return err } defer conn.Close() // Send the packet _, err = conn.Write(packetData) return err } // configureAddress assigns virtual IP address func (c *BasicConfigurer) configureAddress() bool { log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name) var ( ip = binary.LittleEndian.Uint32(c.VIP.AsSlice()) mask = binary.LittleEndian.Uint32(c.Netmask) nteinstance uint32 ) iface, err := net.InterfaceByName(c.Iface.Name) if err != nil { log.Error("Failed to access interface: ", err) return false } err = iphlpapi.AddIPAddress(ip, mask, uint32(iface.Index), &c.ntecontext, &nteinstance) if err != nil { log.Error("Failed to add address: ", err) return false } if buff, err := c.createGratuitousARP(); err != nil { log.Warn("Failed to compose gratuitous ARP request: ", err) } else { if err := sendPacketWindows(c.Iface, buff); err != nil { log.Warn("Failed to send gratuitous ARP request: ", err) } } return true } // deconfigureAddress drops virtual IP address func (c *BasicConfigurer) deconfigureAddress() bool { log.Infof("Removing address %s on %s", c.getCIDR(), c.Iface.Name) err := iphlpapi.DeleteIPAddress(c.ntecontext) if err != nil { log.Errorf("Failed to remove address %s: %v", c.getCIDR(), err) return false } return true } vip-manager-4.0.0/ipmanager/hetznerConfigurer.go000066400000000000000000000162431502154252400217240ustar00rootroot00000000000000package ipmanager import ( "bufio" "encoding/json" "errors" "net" "os" "os/exec" "time" ) const ( unknown = iota // c0 == 0 configured = iota // c1 == 1 released = iota // c2 == 2 ) // The HetznerConfigurer can be used to enable vip-management on nodes // rented in a Hetzner Datacenter. // Since Hetzner provides an API that handles failover-ip routing, // this API is used to manage the vip, whenever hostintype `hetzner` is set. type HetznerConfigurer struct { *IPConfiguration cachedState int lastAPICheck time.Time verbose bool } func newHetznerConfigurer(config *IPConfiguration, verbose bool) (*HetznerConfigurer, error) { c := &HetznerConfigurer{ IPConfiguration: config, cachedState: unknown, lastAPICheck: time.Unix(0, 0), verbose: verbose} return c, nil } /** * In order to tell the Hetzner API to route the failover-ip to * this machine, we must attach our own IP address to the API request. */ func getOutboundIP() net.IP { conn, err := net.Dial("udp", "8.8.8.8:80") if err != nil || conn == nil { log.Error("error dialing 8.8.8.8 to retrieve preferred outbound IP", err) return nil } defer conn.Close() localAddr := conn.LocalAddr().(*net.UDPAddr) return localAddr.IP } func (c *HetznerConfigurer) curlQueryFailover(post bool) (string, error) { /** * The credentials for the API are loaded from a file stored in /etc/hetzner . */ //TODO: make credentialsFile dynamically changeable? credentialsFile := "/etc/hetzner" f, err := os.Open(credentialsFile) if err != nil { log.Error("can't open passwordfile", err) return "", err } defer f.Close() /** * The retrieval of username and password from the file is rather static, * so the credentials file must conform to the offsets down below perfectly. */ var user string var password string scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() switch line[:4] { case "user": user = line[6 : len(line)-1] case "pass": password = line[6 : len(line)-1] } } if user == "" || password == "" { log.Infoln("Couldn't retrieve username or password from file", credentialsFile) return "", errors.New("Couldn't retrieve username or password from file") } /** * As Hetzner API only allows IPv4 connections, we rely on curl * instead of GO's own http package, * as selecting IPv4 transport there doesn't seem trivial. * * If post is set to true, a failover will be triggered. * If it is set to false, the current state (i.e. route) * for the failover-ip will be retrieved. */ var cmd *exec.Cmd if post { myOwnIP := getOutboundIP() if myOwnIP == nil { log.Error("Error determining this machine's IP address.") return "", errors.New("Error determining this machine's IP address") } log.Infof("my_own_ip: %s\n", myOwnIP.String()) cmd = exec.Command("curl", "--ipv4", "-u", user+":"+password, "https://robot-ws.your-server.de/failover/"+c.IPConfiguration.VIP.String(), "-d", "active_server_ip="+myOwnIP.String()) log.Debugf("%s %s %s '%s' %s %s %s", "curl", "--ipv4", "-u", user+":XXXXXX", "https://robot-ws.your-server.de/failover/"+c.IPConfiguration.VIP.String(), "-d", "active_server_ip="+myOwnIP.String()) } else { cmd = exec.Command("curl", "--ipv4", "-u", user+":"+password, "https://robot-ws.your-server.de/failover/"+c.IPConfiguration.VIP.String()) log.Debugf("%s %s %s %s %s", "curl", "--ipv4", "-u", user+":XXXXXX", "https://robot-ws.your-server.de/failover/"+c.IPConfiguration.VIP.String()) } out, err := cmd.Output() if err != nil { return "", err } retStr := string(out[:]) return retStr, nil } /** * This function is used to parse the response which comes from the * curlQueryFailover function and in turn from the curl calls to the API. */ func (c *HetznerConfigurer) getActiveIPFromJSON(str string) (net.IP, error) { var f map[string]interface{} log.Debugf("JSON response: %s\n", str) err := json.Unmarshal([]byte(str), &f) if err != nil { log.Errorln(err) return nil, err } if f["error"] != nil { errormap := f["error"].(map[string]interface{}) log.Errorf("There was an error accessing the Hetzner API!\n"+ " status: %f\n code: %s\n message: %s\n", errormap["status"].(float64), errormap["code"].(string), errormap["message"].(string)) return nil, errors.New("Hetzner API returned error response") } if f["failover"] != nil { failovermap := f["failover"].(map[string]interface{}) ip := failovermap["ip"].(string) netmask := failovermap["netmask"].(string) serverIP := failovermap["server_ip"].(string) serverNumber := failovermap["server_number"].(float64) activeServerIP := failovermap["active_server_ip"].(string) log.Infoln("Result of the failover query was: ", "failover-ip=", ip, "netmask=", netmask, "server_ip=", serverIP, "server_number=", serverNumber, "active_server_ip=", activeServerIP, ) return net.ParseIP(activeServerIP), nil } return nil, errors.New("why did we end up here?") } func (c *HetznerConfigurer) queryAddress() bool { if (time.Since(c.lastAPICheck) / time.Hour) > 1 { /**We need to recheck the status! * Don't check too often because of stupid API rate limits */ log.Info("Cached state was too old.") c.cachedState = unknown } else { /** no need to check, we can use "cached" state if set. * if it is set to UNKNOWN, a check will be done. */ if c.cachedState == configured { return true } else if c.cachedState == released { return false } } str, err := c.curlQueryFailover(false) if err != nil { //TODO c.cachedState = unknown } else { c.lastAPICheck = time.Now() } currentFailoverDestinationIP, err := c.getActiveIPFromJSON(str) if err != nil { //TODO c.cachedState = unknown } if currentFailoverDestinationIP.Equal(getOutboundIP()) { //We "are" the current failover destination. c.cachedState = configured return true } c.cachedState = released return false } func (c *HetznerConfigurer) configureAddress() bool { //log.Printf("Configuring address %s on %s", m.GetCIDR(), m.iface.Name) return c.runAddressConfiguration() } func (c *HetznerConfigurer) deconfigureAddress() bool { //The address doesn't need deconfiguring since Hetzner API // is used to point the VIP address somewhere else. c.cachedState = released return true } func (c *HetznerConfigurer) runAddressConfiguration() bool { str, err := c.curlQueryFailover(true) if err != nil { log.Infof("Error while configuring Hetzner failover-ip! Error message: %s", err) c.cachedState = unknown return false } currentFailoverDestinationIP, err := c.getActiveIPFromJSON(str) if err != nil { c.cachedState = unknown return false } c.lastAPICheck = time.Now() if currentFailoverDestinationIP.Equal(getOutboundIP()) { //We "are" the current failover destination. log.Info("Failover was successfully executed!") c.cachedState = configured return true } log.Infof("The failover command was issued, but the current Failover destination (%s) is different from what it should be (%s).", currentFailoverDestinationIP.String(), getOutboundIP().String()) //Something must have gone wrong while trying to switch IP's... c.cachedState = unknown return false } vip-manager-4.0.0/ipmanager/ip_configuration.go000066400000000000000000000010651502154252400215540ustar00rootroot00000000000000package ipmanager import ( "fmt" "net" "net/netip" ) // IPConfiguration holds the configuration for VIP manager type IPConfiguration struct { VIP netip.Addr Netmask net.IPMask Iface net.Interface RetryNum int RetryAfter int } // getCIDR returns the CIDR composed from the given address and mask func (c *IPConfiguration) getCIDR() string { return fmt.Sprintf("%s/%d", c.VIP.String(), netmaskSize(c.Netmask)) } func netmaskSize(mask net.IPMask) int { ones, bits := mask.Size() if bits == 0 { panic("Invalid mask") } return ones } vip-manager-4.0.0/ipmanager/ip_manager.go000066400000000000000000000055451502154252400203260ustar00rootroot00000000000000package ipmanager import ( "context" "net" "net/netip" "sync/atomic" "time" "github.com/cybertec-postgresql/vip-manager/vipconfig" "go.uber.org/zap" ) type ipConfigurer interface { queryAddress() bool configureAddress() bool deconfigureAddress() bool getCIDR() string } var log *zap.SugaredLogger = zap.L().Sugar() // IPManager implements the main functionality of the VIP manager type IPManager struct { configurer ipConfigurer states <-chan bool shouldSetIPUp atomic.Bool recheckChan chan struct{} } func getMask(vip netip.Addr, mask int) net.IPMask { if vip.Is4() { //IPv4 if mask > 0 && mask < 33 { return net.CIDRMask(mask, 32) } var ip net.IP = vip.AsSlice() return ip.DefaultMask() } return net.CIDRMask(mask, 128) //IPv6 } func getNetIface(iface string) *net.Interface { netIface, err := net.InterfaceByName(iface) if err != nil { log.Fatalf("Obtaining the interface raised an error: %s", err) } return netIface } // NewIPManager returns a new instance of IPManager func NewIPManager(conf *vipconfig.Config, states <-chan bool) (m *IPManager, err error) { vip, err := netip.ParseAddr(conf.IP) if err != nil { return nil, err } vipMask := getMask(vip, conf.Mask) netIface := getNetIface(conf.Iface) ipConf := &IPConfiguration{ VIP: vip, Netmask: vipMask, Iface: *netIface, RetryNum: conf.RetryNum, RetryAfter: conf.RetryAfter, } m = &IPManager{ states: states, } log = conf.Logger.Sugar() m.recheckChan = make(chan struct{}) switch conf.HostingType { case "hetzner": m.configurer, err = newHetznerConfigurer(ipConf, conf.Verbose) case "basic": fallthrough default: m.configurer, err = newBasicConfigurer(ipConf) } if err != nil { m = nil } return } func (m *IPManager) applyLoop(ctx context.Context) { strUpDown := map[bool]string{true: "up", false: "down"} for { isIPUp := m.configurer.queryAddress() shouldSetIPUp := m.shouldSetIPUp.Load() log.Infof("IP address %s is %s, must be %s", m.configurer.getCIDR(), strUpDown[isIPUp], strUpDown[shouldSetIPUp]) if isIPUp != shouldSetIPUp { var isOk bool if shouldSetIPUp { isOk = m.configurer.configureAddress() } else { isOk = m.configurer.deconfigureAddress() } if !isOk { log.Error("Failed to configure virtual ip for this machine") } } select { case <-ctx.Done(): return case <-m.recheckChan: // signal to recheck case <-time.After(time.Duration(10) * time.Second): // recheck every 10 seconds } } } // SyncStates implements states synchronization func (m *IPManager) SyncStates(ctx context.Context, states <-chan bool) { go m.applyLoop(ctx) for { select { case newState := <-states: if m.shouldSetIPUp.Load() != newState { m.shouldSetIPUp.Store(newState) m.recheckChan <- struct{}{} } case <-ctx.Done(): m.configurer.deconfigureAddress() return } } } vip-manager-4.0.0/main.go000066400000000000000000000030641502154252400151770ustar00rootroot00000000000000package main import ( "context" "fmt" "os" "os/signal" "sync" "github.com/cybertec-postgresql/vip-manager/checker" "github.com/cybertec-postgresql/vip-manager/ipmanager" "github.com/cybertec-postgresql/vip-manager/vipconfig" "go.uber.org/zap" ) var ( // vip-manager version definition version = "master" commit = "none" date = "unknown" ) var log *zap.SugaredLogger = zap.L().Sugar() func main() { if (len(os.Args) > 1) && (os.Args[1] == "--version") { fmt.Printf("version: %s\n", version) fmt.Printf("commit: %s\n", commit) fmt.Printf("date: %s\n", date) return } conf, err := vipconfig.NewConfig() if err != nil { log.Fatal(err) } log = conf.Logger.Sugar() defer func() { _ = conf.Logger.Sync() }() lc, err := checker.NewLeaderChecker(conf) if err != nil { log.Fatalf("Failed to initialize leader checker: %s", err) } states := make(chan bool) manager, err := ipmanager.NewIPManager(conf, states) if err != nil { log.Fatalf("Problems with generating the virtual ip manager: %s", err) } mainCtx, cancel := context.WithCancel(context.Background()) go func() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) <-c log.Info("Received exit signal") cancel() }() var wg sync.WaitGroup wg.Add(1) go func() { err := lc.GetChangeNotificationStream(mainCtx, states) if err != nil && err != context.Canceled { log.Fatal("Leader checker returned the following error: %s", zap.Error(err)) } wg.Done() }() wg.Add(1) go func() { manager.SyncStates(mainCtx, states) wg.Done() }() wg.Wait() } vip-manager-4.0.0/test/000077500000000000000000000000001502154252400147005ustar00rootroot00000000000000vip-manager-4.0.0/test/behaviour_test.sh000077500000000000000000000043641502154252400202710ustar00rootroot00000000000000#!/bin/bash set -eu -o pipefail RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color export ETCDCTL_API=3 # testing parameters vip=10.0.2.123 function get_dev { # select a suitable device for testing purposes # * a device that is an "ether" # * and isn't a nil hardware address # strip suffix from name (veth3@if8 -> veth3) ip -oneline link show | grep link/ether | grep -v 00:00:00:00:00:00 | cut -d ":" -f2 | cut -d "@" -f 1 | head -n1 } dev="`get_dev`" # prerequisite test: do we have a suitable device? test -n "$dev" #cleanup function cleanup { if test -f .ncatPid then kill `cat .ncatPid` 2> /dev/null || true rm .ncatPid fi if test -f .vipPid then kill `cat .vipPid` 2> /dev/null || true rm .vipPid fi if test -f .etcdPid then kill `cat .etcdPid` 2> /dev/null || true rm .etcdPid fi if test -f .failed then echo -e "${RED}### Some tests failed! ###${NC}" rm .failed fi } trap cleanup EXIT # prerequisite test: vip should not yet be registered ! ip address show dev $dev | grep $vip # run etcd with podman/docker maybe? # podman rm etcd || true # podman run -d --name etcd -p 2379:2379 -e "ALLOW_NONE_AUTHENTICATION=yes" bitnami/etcd # run etcd locally maybe? etcd & echo $! > .etcdPid sleep 2 # simulate server, e.g. postgres ncat -vlk 0.0.0.0 12345 -e "/bin/echo $HOSTNAME" & echo $! > .ncatPid etcdctl del service/pgcluster/leader || true touch .failed ./vip-manager --interval 3000 --interface $dev --ip $vip --netmask 32 --trigger-key service/pgcluster/leader --trigger-value $HOSTNAME & #2>&1 & echo $! > .vipPid sleep 2 # test 1: vip should still not be registered ! ip address show dev $dev | grep $vip # simulate patroni member promoting to leader etcdctl put service/pgcluster/leader $HOSTNAME sleep 2 # test 2: vip should now be registered ip address show dev $dev | grep $vip ncat -vzw 1 $vip 12345 # simulate leader change etcdctl put service/pgcluster/leader 0xGARBAGE sleep 2 # test 3: vip should be deregistered again ! ip address show dev $dev | grep $vip ! ncat -vzw 1 $vip 12345 rm .failed echo -e "${GREEN}### You've reached the end of the script, all \"tests\" have successfully been passed! ###${NC}" vip-manager-4.0.0/test/cacert_test.sh000077500000000000000000000054611502154252400175450ustar00rootroot00000000000000#!/bin/bash set -eu -o pipefail RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color export ETCDCTL_API=3 # testing parameters vip=10.0.2.123 function get_dev { # select a suitable device for testing purposes # * a device that is an "ether" # * state is UP not DOWN # * and isn't a nil hardware address # strip suffix from name (veth3@if8 -> veth3) ip -oneline link show | grep link/ether | grep state.UP | grep -v 00:00:00:00:00:00 | cut -d ":" -f2 | cut -d "@" -f 1 | head -n1 } dev="`get_dev`" # prerequisite test: do we have a suitable device? test -n "$dev" #cleanup function cleanup { if test -f .ncatPid then kill `cat .ncatPid` 2> /dev/null || true rm .ncatPid fi if test -f .vipPid then kill `cat .vipPid` 2> /dev/null || true rm .vipPid #rm vip-manager.log fi if test -f .etcdPid then kill `cat .etcdPid` 2> /dev/null || true rm .etcdPid fi if test -f .failed then echo -e "${RED}### Some tests failed! ###${NC}" rm .failed fi #podman stop etcd } trap cleanup EXIT # prerequisite test 0: vip should not yet be registered ! ip address show dev $dev | grep $vip # run etcd with podman/docker maybe? # podman rm etcd || true # podman run --rm -d --name etcd -p 2379:2379 -e "ETCD_ENABLE_V2=true" -e "ALLOW_NONE_AUTHENTICATION=yes" -v `pwd`/test/certs/:/certs:Z quay.io/coreos/etcd /usr/local/bin/etcd --cert-file=/certs/etcd_server.crt --key-file=/certs/etcd_server.key --listen-client-urls https://127.0.0.1:2379 --advertise-client-urls https://127.0.0.1:2379 # run etcd locally maybe? #etcd --cert-file=test/certs/etcd_server.crt --key-file=test/certs/etcd_server.key --listen-client-urls https://127.0.0.1:2379 --advertise-client-urls https://127.0.0.1:2379 & #echo $! > .etcdPid #sleep 2 # simulate server, e.g. postgres ncat -vlk 0.0.0.0 12345 -e "/bin/echo $HOSTNAME" & echo $! > .ncatPid etcdctl --cacert test/certs/etcd_server_ca.crt del service/pgcluster/leader || true touch .failed ./vip-manager --etcd-ca-file test/certs/etcd_server_ca.crt --dcs-endpoints https://127.0.0.1:2379 --interface $dev --ip $vip --netmask 32 --trigger-key service/pgcluster/leader --trigger-value $HOSTNAME &> vip-manager.log & echo $! > .vipPid sleep 2 # test 1: vip should still not be registered ! ip address show dev $dev | grep $vip # simulate patroni member promoting to leader etcdctl --cacert test/certs/etcd_server_ca.crt put service/pgcluster/leader $HOSTNAME sleep 2 # we're just checking whether vip-manager picked up the change, for some reason, we can't run an elevated container of quay.io/coreos/etcd grep 'state is false, desired true' vip-manager.log rm .failed echo -e "${GREEN}### You've reached the end of the script, all \"tests\" have successfully been passed! ###${NC}" vip-manager-4.0.0/test/certs/000077500000000000000000000000001502154252400160205ustar00rootroot00000000000000vip-manager-4.0.0/test/certs/etcd_client.crt000066400000000000000000000037751502154252400210230ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIUSVGhOPULK6WMRVVq9zQ5Dm4bBv0wDQYJKoZIhvcNAQEL BQAwIjEgMB4GA1UEAwwXaS5hbS50aGUuZXRjZC5zZXJ2ZXIuY2EwHhcNMjAwNDAz MTc0NTI1WhcNMzAwNDAxMTc0NTI1WjAeMRwwGgYDVQQDDBNjZW50b3Mtc2VydmVy LTctMTAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtUEiDqKo5UPq VvrJQSB8lYgYw371/wJos+EHsI1H6nmnzFtMYOpwr9OBsu5VHThyNd+hAd7ktLaB wR3NeRUsCljiVcle41k6chlTDEy82UUbuL3wQHh52lMqb4NSC8NnGyEp8rp76Db/ +WiFaQrn00er1buW872WV4gsKBiXlJZoucS7EkPOoLwQ9AmYKv4kBIhU9r1rvMn8 PQDhHRHzHlnysk9DLdpi+HmsBS0/qYQxPh4Chq9fSnU8eyXxR2HVPwuGBs2kaTcb VYsFGOjn+86PgiXBvw2W4ogaDoDaxt91TzsCrU4dUCnfKSo8Qb5TpuZDRfLxdhZc jfYOwqkoVUAmPUtSdyDk1u5fTM4Po5RZlyVf9Ucm7M/uK5AEUqILNpsPtj2+Wjjw TPnltYCSMGi9dHNqSpWXQUmIO7Mu1Ez9jF5WBmUlEmRQ4eGSEdl+EA6jpkgkIi6y 89q80wHW8c4WteUmJuAn7jGS/9E63uug/NeBYBY8B5azfZ3Yn4y3K7APA7/0mBFL slZBHZzngfw4Xy0RgEzMzybnyAIK7oVj675KaLvarQxzg5B4OD7L8mR0Hd706IEY eQNq/y8V53JlycOyG6dJTq75CPN5a/VEbjCEHxBdam1oWDwHaUhWPhepnGqi/i43 9uJp+/JdmG92S4RSGUCDRXGZ7E/p/lECAwEAAaOB6zCB6DBpBgNVHREEYjBghwR/ AAABhwTAqLJIhwR/AAABhwTAqLJIhxAAAAAAAAAAAAAAAAAAAAABhxAgAwDd/wrs AFCjhjpN1z7KhxAAAAAAAAAAAAAAAAAAAAABhxAgAwDd/wrsAFCjhjpN1z7KMA4G A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYD VR0TAQH/BAIwADAdBgNVHQ4EFgQUk/gJR3Fn1QVISRqaecmdex/FC+QwHwYDVR0j BBgwFoAU4Ofj09tTKVnH+KHTVZUXX1TsceQwDQYJKoZIhvcNAQELBQADggIBACUr q6T6Pjx8GZiV2c+qozqBoDGqPGQ94iPwXEOA32t1VT/VBqpEv5BNpwRV2y8RtjJQ hKCtk3DnsmxSJuMOB4HLlE/C9xuyyxVyvfP+LjExGi55/ITQOUXwtQRt2wAu2PSy ZnwFfTHfR2/7yVkq0scRJ5AXPe/uvAfJY5TNQl7rBrXwDKvmnvGzQOH9o/dKGui3 Sz7r0t2RXd2WGL0x+IiVrOXwti9ktp0s58Vo9JYpmimTVkPjWV9YdzhkIJRj7hxf J68PsmTE+UHkCkv3lqgpK1DDKfa23a4XVYR22Ccc3VqriVd27RTpr++3ZH2cNX6Y UJQtJFLkkcrSZlKJFlT+9heg7qn8IGA8BL+xXeCw9BDpq5TlEJ01BPGeKuJ1Ks1X YyYA8EItcOjQXQzzyIZculQzIEC9+3ZjHxIUKjKN5LeR+2fbD8dWRK8w9ylRMJoC kUlniZsi/weCxGPu5cg9mLSjGcPTNbDxb6WvCRb+8gG880f3deqHq0UIep6NG3jv pdUIwNKB1BuFvVMPHl1Oqqdm35AAsgv55WQgd3xk9i0ltJza+SPQlp/TqxDbZEFm rPnLHG9oSlzWVrO++/D+xc2qsLjTq8ApqyV9J4w63ksX6q/Qkr/MZJncNs+WKIx5 J5DXiD3fK3F91+BHftaJpiHpe1Nx/DloAmPweC2u -----END CERTIFICATE----- vip-manager-4.0.0/test/certs/etcd_client.key000066400000000000000000000062531502154252400210150ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEAtUEiDqKo5UPqVvrJQSB8lYgYw371/wJos+EHsI1H6nmnzFtM YOpwr9OBsu5VHThyNd+hAd7ktLaBwR3NeRUsCljiVcle41k6chlTDEy82UUbuL3w QHh52lMqb4NSC8NnGyEp8rp76Db/+WiFaQrn00er1buW872WV4gsKBiXlJZoucS7 EkPOoLwQ9AmYKv4kBIhU9r1rvMn8PQDhHRHzHlnysk9DLdpi+HmsBS0/qYQxPh4C hq9fSnU8eyXxR2HVPwuGBs2kaTcbVYsFGOjn+86PgiXBvw2W4ogaDoDaxt91TzsC rU4dUCnfKSo8Qb5TpuZDRfLxdhZcjfYOwqkoVUAmPUtSdyDk1u5fTM4Po5RZlyVf 9Ucm7M/uK5AEUqILNpsPtj2+WjjwTPnltYCSMGi9dHNqSpWXQUmIO7Mu1Ez9jF5W BmUlEmRQ4eGSEdl+EA6jpkgkIi6y89q80wHW8c4WteUmJuAn7jGS/9E63uug/NeB YBY8B5azfZ3Yn4y3K7APA7/0mBFLslZBHZzngfw4Xy0RgEzMzybnyAIK7oVj675K aLvarQxzg5B4OD7L8mR0Hd706IEYeQNq/y8V53JlycOyG6dJTq75CPN5a/VEbjCE HxBdam1oWDwHaUhWPhepnGqi/i439uJp+/JdmG92S4RSGUCDRXGZ7E/p/lECAwEA AQKCAgBZKmGGsZ5EqtRtVZIL599h8EG+aoa5nIXFd75ArD/kqVRSw9cfFjW5SWNU kspsRYhp2ElskioQfHf4eKDMIA46SN+PNDDpxstpteuU8Ws1tzmb+FRoYtwO+zq1 APUrtETUo8vvDK5H3kaueyymMCc7WNa/njj0Tx/Wj7apQu/OuO9r88vTgGf1yo7M fqvM+pjdYfPqLeUCSps7p2MW2e2v0LAD59o0hGqLsc9d/JSE3/MZi8nSWBOYnXPT YScA5q/xA8o7Lo7i86kOvAUV6/2zcjpG3CwjwJupdSrcV6dIjkU3ZOA8QWFKPk76 AT0DWo3sXPpbPthqLzX44EMCoBSmh6BQ9gPUZDhdK7swwnkksXs84G5LODPwiNPg ZF7SgRbjcTQmzOXFqmrZcMR2itUcttpFkUnN6fH615DDunk8FCnOxUXdplenliN4 7MHL4NSr6KOclmRDpew5Wl++/wxrM/V7Y7LBk/OuzcR2BDyG1JkzaOUlUjpXsuy5 l2rzIT9i7WQ8IKyEO/HGv9l8nXRlZPWVJOglL0UT0PWfOeY9Cd/zBt63SMjYnXSP HJzv/ooT/cNfnQwWmUzB/Z9bK2sutOuwBayYGnqt+j2QT0x2hq+p4v6P56A82WZ+ ubjYhSnJzyqeDx7Xf6zg2PZ/kIFFDI6UnlnTDAurE4BdVxdhwQKCAQEA2xXRfAWs eSehfpLAVoETTu21AOOi6mgLC8h4XrpSbKUDFIDW/fVxzZ3sSELgLu715GKl+6qs NqW2zN9K3cSiFZ1oEaj5v2gZhSxF0A/36mD586QwAkFXDE6EYAVwttcogSKC2VB5 IYEEiR/aAGo8FxN5LHXiotnlqJm0fUebwzlcX8zr+YLo8f4lnyDOLGmOTEWdGHTb 3P+fV/lJSu2B2sQow92AXJKOlRHr9YfScgu/pXMOaB6yv3pcnQHr/VzIIC2pY9pR ye1Wjvxwg+StFMSN9SgaUHUGgkq9WC4Z1IgYNJBFyc0k7MS9GkLVt3TFdjZEOa8/ rORu1D4q8eSPpwKCAQEA08t+TvG+RIqBgWRl3P0qKv8EWkPR/jqu+RdFOzR5qpbT CFcI6YwILfQ0hCt6W9X8zdwKuS7KjbKdV1fLhQSiKl64kiJKbCTJPxpThcxa9hjK 6jcxtxQCEJtTWXwbB3JNLCuY3XopsIIME4ZNQh7vr1Pl/GoG+/Yw0PjH8229Lda1 rPIayQseOKolSTqClZTswKGfL7HHcV8jsN+hBZYw9BTL8cW6ctiPfP5lSQZnXDvs C66grP4zbTtOP0TsfIDjGZnJVhZQOXJDK7Xfm3AcGsPXccurg26kw1LF9RExfCwL Q12VsYDNMq2JRFjBzqifB1LSdtxkI3KySSmQpiWBRwKCAQEApL/Uh2Iw3+7Yd7ld n+9ymKESwzdrdMCGxfab5ghRIVg5Z7q3ccSYLtp6K6D2uvSBvpwcW5Nt665UN94W i4xpor163ATownJC9q1jVmIbuYnxjLFEVP3TuvJ0g0y0BRrpX6qXIVptrK87vO5R 3owE4gmHztJbesFG/bGQU8F8taM1/ui37yrth8Tpf6+Iu0cpddvHlfOSvq8PoXVa E1lllCB800WHWJXxWNJgUYQw0ghZts494DhtjKY0bPFcCGw0JlaQEgHEDYhH1kCp T9Wv/nUMl3Xvy98k5OfVWTFZxUQOh8CSan21LcOIvO3TjyDluM54IbTSum4RldOm Mb8B8wKCAQEAoWZnGBQjkioW7RssgU8wjlmO4JbkdaAU7WAtcyPXQAf2RFnHQetj 5FlAmCRl94xIPjzcsyiUVY1zWDdgsjrItg8/CqY1Htqdvof6dHE6NGbKY9ix+zm1 JSCpUP1Bv9f8NZf3w3gwQwGn4E0tnSDkOTFvh37pWaPQqb+c6MaNL1x7UJOjk+f0 HOyUw4xiLUmzbkz8eaU6Pwxor4aMOCyvm6IplVLAdnrQRkm7t/24UNKdXH20loCV gj16sL5+lZbG+iB3DTKt6klIJQxRniu+TytFiMPULbHov6zZjJuQoXcTEkBELmPg fClA/SPCdhGMN1GHb+seKOFkOlsBj4vvSwKCAQBjzuiiUf/eaciq0iDGBnH8vQSy fPN4bn1NWbgShnPCLLXZV95PqBwEwUf6rfB2mWlAvt48jT7wvszf9iV+7FJfstjl vzjqx5KOfPP38lhVZ/h1XfID3n/RHhcwMlXlM07zvyo2SqaAr4tNxPVxoFyHBb+N vOYEEVqlXkgLU7XJsYWAh9d1vuJhsrX+p85vj3aPmwX/sKY6jtRILOAglWkp/6T/ 1CgWcziSofxKqkON5OSpP4kghzYBPbW7O7X1r4NNg7LsAltf7YTDmltVAV6AOdlB UVbnClsnX2fpacfcUTB0rpzzTav1+uro4UjTRiNabjpFeF+HiuaDXMScVeio -----END RSA PRIVATE KEY----- vip-manager-4.0.0/test/certs/etcd_server.crt000066400000000000000000000036741502154252400210510ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFijCCA3KgAwIBAgIULNyH/+7i9YuYR1/hm0pZ/MqKOzMwDQYJKoZIhvcNAQEL BQAwIjEgMB4GA1UEAwwXaS5hbS50aGUuZXRjZC5zZXJ2ZXIuY2EwHhcNMjAwNDAz MTc0NTE3WhcNMzAwNDAxMTc0NTE3WjAeMRwwGgYDVQQDDBNjZW50b3Mtc2VydmVy LTctMTAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoCHqFpy9G3MF lYNejKpfLQqQvHEW612+GtRIjUATYuuR49rrXFpKids+jNaDDMwx2wObaO4ECP5E UP7OkAcn0EaZ6IdImFPnpRZ0T6CKevPC+v2OPP2iN57mJsXI2Qve7XrEez3C/qdv jWZK+PIp77OCry5jblLxQgsNmwmNb6gUBGiqqWDBif3mZQZAMJtnRbnVWnHweA2A 4m3Z2j7DCFQoZXBEUEdp4GhFwz8/OWjK52GonA1bJS7MYPPUyU9wZthgCoI3scm7 hIEBiole6kJcLKEcHE/CTSUdqgZQUhOpLTTnXlhnDhyRngbdc/hmJhKmca9nh9Sz IRJeU1rCnZVi1rjKjdAT2ta7nJXAO+fk+bSYL+XbN4iRlIXrb9JiQehkFIhu1vQp VpLZT430nymBtG34ldHRZmIEEYA0LMHWWIs0dc2jn4SWFpXOdgOvKWdK5mMFT+gN PqHrJ/goy+214+5uikpdNlj8J2GT038duM+OB7Or+mh2XAI94RbV86Mr1kKzgMCT GdMV0WOvhkfRKSNx4WuQa7A8ToCuNJLYSbgaRAIaTraY0NQz4MpMuM+Skp8j52bn FedwmxkclnBpsNEN43mRogc5eDyZqZdgsf0WZq/ZzayQmZiGR+whNXg5Y/fZ19Up uqGyofEWpCivvC/XsGcbXe5dUBynkQcCAwEAAaOBuzCBuDA5BgNVHREEMjAwhwR/ AAABhwTAqLJIhxAAAAAAAAAAAAAAAAAAAAABhxAgAwDd/wrsAFCjhjpN1z7KMA4G A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYD VR0TAQH/BAIwADAdBgNVHQ4EFgQU6yz/154zOfY7a15DgNWuSvmL3BowHwYDVR0j BBgwFoAU4Ofj09tTKVnH+KHTVZUXX1TsceQwDQYJKoZIhvcNAQELBQADggIBAK/g eJzkNrGT29aUI2zSM3UgKLet/eov/EUfjWB/2wi028mEREsNKNRRwDH1gC/h5kkh qryrf4rDb3WiaqCiM+jxhxhJY491NAbTXs1HFNW0a2YxHOqgceBB4uOWzM8BbKOg hYtxEGMAUR0kIfRTJO/t5WD2wFL75pdgEOdIQoabN2Yiur3+frrZhx/tjaLf6nnr YD8lWFt1+wfW2QfmYLwXYzdR/F39WAVbNwWzimYthow7EekiBEoH57Yv/4hFoR4Z FoHC8cRudbkiVNDXVN9hNqfMCmxeq0HrQOaa4gHnw85C4gSZdR7Q2hwDHF3Ujbqf /mS94eGa2qLIFQW7gvvQf4mTRTjO51owZ6Yt6NBS9GzMGUsKdlC0ylbBiuK/7wBh iB8rQdayLp/+6xFhGD8D8G+Xj0VvVlQ1tos2JdvJ1LhoTXWG2FjZ9ZpIXOU6Xjvj N9MufxgDidcXJOdA3lTKVBhmLobjvTcxepkmSEP5sS/Yi2DuFSohpcjS1GLhGrMu 9Q6rpVPjRauZAbYigjI3Rt1lryjgwTr+Rlyx0okmH2A7jNCOXN8EI7sZFHeKgpgq 3nPwEnvOem0v1joTyuIwcndWQyegwa69K+lowcb43C5akBzDZs7myPvU6MzYhoN7 I5PoSbsOrshivg/+KnCBxoPswpvJN/l26o+8UGb1 -----END CERTIFICATE----- vip-manager-4.0.0/test/certs/etcd_server.key000066400000000000000000000062531502154252400210450ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- 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 -----END RSA PRIVATE KEY----- vip-manager-4.0.0/test/certs/etcd_server_ca.crt000066400000000000000000000035531502154252400215100ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFTTCCAzWgAwIBAgIUEvgw+Vm0JRrpoyccisxPPIxVLgowDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJaS5hbS5hLmNhMB4XDTIwMDQwMzE3NDUwOVoXDTMwMDQw MTE3NDUwOVowIjEgMB4GA1UEAwwXaS5hbS50aGUuZXRjZC5zZXJ2ZXIuY2EwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6+76EfGNVBlkzH8eWCsYSWok9 lE3BstEvNpFpaZyEjW4/Q3TVZsvhl70e4Ko8SYIdc7ZI2FHPlhJpqqxjldfAx2O6 V41qgutt6K0X5/2yY0XGlIrGOCVLVKfYZmCgheQ7pwwdh6+PBBCsJd34LE0tZx4/ rgVw1Z+pcqP+9UBx/8pGgi7O7e+ENxKuZNcgjG9LKC3k558fKpdeg5UyuBPLu49C xhM1h8zNk7ExyXlplIOmGr1y1MDBAkqikl9cxs10G66Nzvtar08sSP3UMlVM1fqq vAVFARAyqbAPxa3Mrv712/AywQV7Lr6n3LJFPlteJ7MIs/+1xAAAmLH/By8O8saH 0nyoR+498VPbQPD1RxxBeia1bNE/zrmAO9UIdUg3uvUuZoIdQpypV1t9zNsd1duQ aMWLjPNl1LtYJWg2UCS/yrSsSnqPZhYECL9a9zBKjqMcya6B4ZRAbWY6Kj6sUvpu GZuA+i2ICpmg9f3gypOt+nrrN2wUJ7qFFy0CHQaHNn8SumuHWTmoK1XIuSIPVKfO tqxJu7b22T9IWOrR0m+M6hWs1IYEzv4dCKRLHzbI0p+PYCKTfYqNZw0GOSS1Zf2/ Qs8yrJDNes7qqAtE7Js9LOxIz1U18sLFwQU4JCo8J0+EeX4MNvpDoP/Hfgo4C8gp ZeuaBJfxfJa13QRBfwIDAQABo4GIMIGFMCIGA1UdEQQbMBmCF2kuYW0udGhlLmV0 Y2Quc2VydmVyLmNhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0G A1UdDgQWBBTg5+PT21MpWcf4odNVlRdfVOxx5DAfBgNVHSMEGDAWgBTjygVZXrWb FUh/t4mYQWLPSQTOozANBgkqhkiG9w0BAQsFAAOCAgEAOBdbChFLkEwT+X+CyYy8 P9AD/U5MBhanjArZjKnmDNtWbaFujU9P4MhVZSRQybtCFixujy4svC2yww/y0VbC AY80U5KELfR4eW39wyASSmlL3F3AhciyTjDlwrnGvt32uahwHeIvq4a43g5DZQi0 RJnyj6zErhUpeEYOD5xvmnK+e6bDdkH4QLkOAcEFHa4fU4QrghHHjfi8AAuZyTk8 GA+ViodUMDI8APvXaXpljQarttnmFz76pwYhDyteKQzhJTIKO2lxdDn1Iutx5GH+ VO4yl5nhaPKrHscuouralCtZDKUMMz2Le40oSnC72qGS4KimOD52O4XrqISO5DtD HtXh1P5xcpnCXlnbClcwoI/46rGNcXAwlXy01iPeH2bxrl0xhdJpKpVmWI11LuYe vzk3gmttGdCvbYE496hiEGX8yVW43LC6tcXUOGrPKODIj5iQCPBHc5/Xo7OvKkTr 9jnZAyKogb21VoJbECcXrOCnwnfkKhfajbFYOnJkntR0Z6Eae8d3yj7oA5EgPgEa 2ucBelrMbBrgHV2vkvkQ7/wxhYfMn9Rhw+UGaem99hN9+1eBEm/azP5fT/Kt7oN7 KNl9c5Wgto7rzy49bmgb7xWqGtHkQ7v1SUTErGc+IiMU/5oDQzcf3EpfIW3TDX9P 1qq8yN3Rs4zlP7uBE7Kaj8c= -----END CERTIFICATE----- vip-manager-4.0.0/test/clientcert_test.sh000077500000000000000000000062331502154252400204360ustar00rootroot00000000000000#!/bin/bash set -eu -o pipefail RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color export ETCDCTL_API=3 # testing parameters vip=10.0.2.123 function get_dev { # select a suitable device for testing purposes # * a device that is an "ether" # * state is UP not DOWN # * and isn't a nil hardware address # strip suffix from name (veth3@if8 -> veth3) ip -oneline link show | grep link/ether | grep state.UP | grep -v 00:00:00:00:00:00 | cut -d ":" -f2 | cut -d "@" -f 1 | head -n1 } dev="`get_dev`" # prerequisite test: do we have a suitable device? test -n "$dev" #cleanup function cleanup { if test -f .ncatPid then kill `cat .ncatPid` 2> /dev/null || true rm .ncatPid fi if test -f .vipPid then kill `cat .vipPid` 2> /dev/null || true rm .vipPid #rm vip-manager.log fi if test -f .etcdPid then kill `cat .etcdPid` 2> /dev/null || true rm .etcdPid fi if test -f .failed then echo -e "${RED}### Some tests failed! ###${NC}" rm .failed fi #podman stop etcd } trap cleanup EXIT # prerequisite test 0: vip should not yet be registered ! ip address show dev $dev | grep $vip # run etcd with podman/docker maybe? # podman rm etcd || true # podman run --rm -d --name etcd -p 2379:2379 -e "ETCD_ENABLE_V2=true" -e "ALLOW_NONE_AUTHENTICATION=yes" -v `pwd`/test/certs/:/certs:Z quay.io/coreos/etcd /usr/local/bin/etcd --trusted-ca-file=/certs/etcd_server_ca.crt --client-cert-auth --cert-file=/certs/etcd_server.crt --key-file=/certs/etcd_server.key --listen-client-urls https://127.0.0.1:2379 --advertise-client-urls https://127.0.0.1:2379 # run etcd locally maybe? #etcd --enable-v2 --trusted-ca-file=test/certs/etcd_server_ca.crt --client-cert-auth --cert-file=test/certs/etcd_server.crt --key-file=test/certs/etcd_server.key --listen-client-urls https://127.0.0.1:2379 --advertise-client-urls https://127.0.0.1:2379 & #echo $! > .etcdPid sleep 2 # simulate server, e.g. postgres ncat -vlk 0.0.0.0 12345 -e "/bin/echo $HOSTNAME" & echo $! > .ncatPid etcdctl --cert test/certs/etcd_client.crt --key test/certs/etcd_client.key --cacert test/certs/etcd_server_ca.crt del service/pgcluster/leader || true touch .failed ./vip-manager --etcd-cert-file test/certs/etcd_client.crt --etcd-key-file test/certs/etcd_client.key --etcd-ca-file test/certs/etcd_server_ca.crt --dcs-endpoints https://127.0.0.1:2379 --interface $dev --ip $vip --netmask 32 --trigger-key service/pgcluster/leader --trigger-value $HOSTNAME &> vip-manager.log & echo $! > .vipPid sleep 2 # test 1: vip should still not be registered ! ip address show dev $dev | grep $vip # simulate patroni member promoting to leader etcdctl --cert test/certs/etcd_client.crt --key test/certs/etcd_client.key --cacert test/certs/etcd_server_ca.crt put service/pgcluster/leader $HOSTNAME sleep 2 # we're just checking whether vip-manager picked up the change, for some reason, we can't run an elevated container of quay.io/coreos/etcd grep 'state is false, desired true' vip-manager.log rm .failed echo -e "${GREEN}### You've reached the end of the script, all \"tests\" have successfully been passed! ###${NC}" vip-manager-4.0.0/vip-manager.service000066400000000000000000000010531502154252400175100ustar00rootroot00000000000000# This is an example of a systemD config file for vip-manager. # You can copy it to "/etc/systemd/system/vip-manager.service", adjust as necessary and then call # systemctl daemon-reload && systemctl start vip-manager && systemctl enable vip-manager # to start and also enable auto-start after reboot. [Unit] Description=Manages Virtual IP for Patroni After=network-online.target Before=patroni.service [Service] Type=simple ExecStart=/usr/bin/vip-manager --config=/etc/default/vip-manager.yml Restart=on-failure [Install] WantedBy=multi-user.target vip-manager-4.0.0/vipconfig/000077500000000000000000000000001502154252400157055ustar00rootroot00000000000000vip-manager-4.0.0/vipconfig/config.go000066400000000000000000000303441502154252400175050ustar00rootroot00000000000000package vipconfig import ( "errors" "fmt" "os" "sort" "strings" "github.com/spf13/pflag" "github.com/spf13/viper" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Config represents the configuration of the VIP manager type Config struct { IP string `mapstructure:"ip"` Mask int `mapstructure:"netmask"` Iface string `mapstructure:"interface"` HostingType string `mapstructure:"manager-type"` TriggerKey string `mapstructure:"trigger-key"` TriggerValue string `mapstructure:"trigger-value"` //hostname to trigger on. usually the name of the host where this vip-manager runs. EndpointType string `mapstructure:"dcs-type"` Endpoints []string `mapstructure:"dcs-endpoints"` EtcdUser string `mapstructure:"etcd-user"` EtcdPassword string `mapstructure:"etcd-password"` EtcdCAFile string `mapstructure:"etcd-ca-file"` EtcdCertFile string `mapstructure:"etcd-cert-file"` EtcdKeyFile string `mapstructure:"etcd-key-file"` ConsulToken string `mapstructure:"consul-token"` Interval int `mapstructure:"interval"` //milliseconds RetryAfter int `mapstructure:"retry-after"` //milliseconds RetryNum int `mapstructure:"retry-num"` Verbose bool `mapstructure:"verbose"` Logger *zap.Logger } func defineFlags() { // When adding new flags here, consider adding them to the Config struct above // and then make sure to insert them into the conf instance in NewConfig down below. pflag.String("config", "", "Location of the configuration file.") pflag.Bool("version", false, "Show the version number.") pflag.String("ip", "", "Virtual IP address to configure.") pflag.String("netmask", "", "The netmask used for the IP address. Defaults to -1 which assigns ipv4 default mask.") pflag.String("interface", "", "Network interface to configure on .") pflag.String("trigger-key", "", "Key in the DCS to monitor, e.g. \"/service/batman/leader\".") pflag.String("trigger-value", "", "Value to monitor for.") pflag.String("dcs-type", "etcd", "Type of endpoint used for key storage. Supported values: etcd, consul, patroni.") // note: can't put a default value into dcs-endpoints as that would mess with applying default localhost when using consul pflag.String("dcs-endpoints", "", "DCS endpoint(s), separate multiple endpoints using commas. (default \"http://127.0.0.1:2379\", \"http://127.0.0.1:8500\" or \"http://127.0.0.1:8008/\" depending on dcs-type.)") pflag.String("etcd-user", "", "Username for etcd DCS endpoints.") pflag.String("etcd-password", "", "Password for etcd DCS endpoints.") pflag.String("etcd-ca-file", "", "Trusted CA certificate for the etcd server.") pflag.String("etcd-cert-file", "", "Client certificate used for authentiaction with etcd.") pflag.String("etcd-key-file", "", "Private key matching etcd-cert-file to decrypt messages sent from etcd.") pflag.String("consul-token", "", "Token for consul DCS endpoints.") pflag.Int("interval", 1000, "DCS scan interval in milliseconds.") pflag.String("manager-type", "basic", "Type of VIP-management to be used. Supported values: basic, hetzner.") pflag.Int("retry-after", 250, "Time to wait before retrying interactions with outside components in milliseconds.") pflag.Int("retry-num", 3, "Number of times interactions with outside components are retried.") pflag.Bool("verbose", false, "Be verbose. Currently only implemented for manager-type=hetzner .") pflag.CommandLine.SortFlags = false } func mapDeprecated() error { deprecated := map[string]string{ // "deprecated" : "new", "mask": "netmask", "iface": "interface", "key": "trigger-key", "nodename": "trigger-value", "etcd_user": "etcd-user", "etcd_password": "etcd-password", "type": "dcs-type", "endpoint": "dcs-endpoints", "endpoints": "dcs-endpoints", "hostingtype": "manager-type", "hosting_type": "manager-type", "endpoint_type": "dcs-type", "retry_num": "retry-num", "retry_after": "retry-after", "consul_token": "consul-token", "host": "trigger-value", } complaints := []string{} errors := false for k, v := range deprecated { if viper.IsSet(k) { if _, exists := os.LookupEnv("VIP_" + strings.ToUpper(k)); !exists { // using deprecated key in config file (as not exists in ENV) complaints = append(complaints, fmt.Sprintf("Parameter \"%s\" has been deprecated, please use \"%s\" instead", k, v)) } else { if strings.ReplaceAll(k, "_", "-") != v { // this string is not a direct replacement (e.g. etcd-user replaces etcd-user, i.e. in both cases VIP_ETCD_USER is the valid env key) // for example, complain about VIP_IFACE, but not VIP_CONSUL_TOKEN or VIP_ETCD_USER... complaints = append(complaints, fmt.Sprintf("Parameter \"%s\" has been deprecated, please use \"%s\" instead", "VIP_"+strings.ToUpper(k), "VIP_"+strings.ReplaceAll(strings.ToUpper(v), "-", "_"))) } else { continue } } if viper.IsSet(v) { // don't forget to reset the desired replacer when exiting replacer := strings.NewReplacer("-", "_") defer viper.SetEnvKeyReplacer(replacer) // Check if there is only a collision because ENV vars always use _ instead of - and the deprecated mapping only maps from *_* to *-*. testReplacer := strings.NewReplacer("", "") // just don't replace anything viper.SetEnvKeyReplacer(testReplacer) if viper.IsSet(v) { complaints = append(complaints, fmt.Sprintf("Conflicting settings: %s or %s and %s or %s are both specified…", k, "VIP_"+strings.ToUpper(k), v, "VIP_"+strings.ReplaceAll(strings.ToUpper(v), "-", "_"))) if viper.Get(k) == viper.Get(v) { complaints = append(complaints, fmt.Sprintf("… But no conflicting values: %s and %s are equal…ignoring.", viper.GetString(k), viper.GetString(v))) continue } complaints = append(complaints, fmt.Sprintf("…conflicting values: %s and %s", viper.GetString(k), viper.GetString(v))) errors = true continue } } // if this is a valid mapping due to deprecation, set the new key explicitly to the value of the deprecated key. viper.Set(v, viper.Get(k)) // "unset" the deprecated setting so it will not show up in our config later viper.Set(k, "") } } for c := range complaints { fmt.Println(complaints[c]) } if errors { panic("Cannot continue due to conflicts.") } return nil } func setDefaults() { defaults := map[string]any{ "hostingtype": "basic", "dcs-type": "etcd", "interval": 1000, "retry-after": 250, "retry-num": 3, } for k, v := range defaults { if !viper.IsSet(k) { viper.SetDefault(k, v) } } // apply defaults for endpoints if !viper.IsSet("dcs-endpoints") { fmt.Println("No dcs-endpoints specified, trying to use localhost with standard ports!") switch viper.GetString("dcs-type") { case "consul": viper.Set("dcs-endpoints", []string{"http://127.0.0.1:8500"}) case "etcd", "etcd3": viper.Set("dcs-endpoints", []string{"http://127.0.0.1:2379"}) case "patroni": viper.Set("dcs-endpoints", []string{"http://127.0.0.1:8008/"}) } } // set trigger-key to '/leader' if DCS type is patroni and nothing is specified if viper.GetString("trigger-key") == "" && viper.GetString("dcs-type") == "patroni" { viper.Set("trigger-key", "/leader") } // set trigger-value to default value if nothing is specified if triggerValue := viper.GetString("trigger-value"); triggerValue == "" { var err error if viper.GetString("dcs-type") == "patroni" { triggerValue = "200" } else { triggerValue, err = os.Hostname() } if err != nil { fmt.Printf("No trigger-value specified, hostname could not be retrieved: %s", err) } else { fmt.Printf("No trigger-value specified, instead using: %v", triggerValue) viper.Set("trigger-value", triggerValue) } } // set retry-num to default if not set or set to zero if retryNum := viper.GetInt("retry-num"); retryNum <= 0 { viper.Set("retry-num", 3) } } func checkSetting(name string) bool { if !viper.IsSet(name) { fmt.Printf("Setting %s is mandatory", name) return false } return true } func checkMandatory() error { mandatory := []string{ "ip", "netmask", "interface", "trigger-key", "trigger-value", "dcs-endpoints", } success := true for _, v := range mandatory { success = checkSetting(v) && success } if !success { return errors.New("one or more mandatory settings were not set") } return checkImpliedMandatory() } // if reason is set, but implied is not set, return false. func checkImpliedSetting(implied string, reason string) bool { if viper.IsSet(reason) && !viper.IsSet(implied) { fmt.Printf("Setting %s is mandatory when setting %s is specified.", implied, reason) return false } return true } // Some settings imply that another setting must be set as well. func checkImpliedMandatory() error { mandatory := map[string]string{ // "implied" : "reason" "etcd-user": "etcd-password", "etcd-key-file": "etcd-cert-file", "etcd-ca-file": "etcd-cert-file", } success := true for k, v := range mandatory { success = checkImpliedSetting(k, v) && success } if !success { return errors.New("one or more implied mandatory settings were not set") } return nil } func printSettings() { s := []string{} for k, v := range viper.AllSettings() { if v != "" { switch k { case "etcd-password": fallthrough case "consul-token": s = append(s, fmt.Sprintf("\t%s : *****\n", k)) default: s = append(s, fmt.Sprintf("\t%s : %v\n", k, v)) } } } sort.Strings(s) fmt.Println("This is the config that will be used:") for k := range s { fmt.Print(s[k]) } } func loadConfigFile() error { if viper.IsSet("config") { viper.SetConfigFile(viper.GetString("config")) if err := viper.ReadInConfig(); err != nil { return err } fmt.Printf("Using config from file: %s\n", viper.ConfigFileUsed()) } return mapDeprecated() } // NewConfig returns a new Config instance func NewConfig() (*Config, error) { var err error defineFlags() pflag.Parse() // import pflags into viper _ = viper.BindPFlags(pflag.CommandLine) // make viper look for env variables that are prefixed VIP_... // e.g.: viper.getString("ip") will return the value of env variable VIP_IP viper.SetEnvPrefix("vip") viper.AutomaticEnv() //replace dashes (in flags) with underscores (in ENV vars) // so that e.g. viper.GetString("dcs-endpoints") will return value of VIP_DCS_ENDPOINTS replacer := strings.NewReplacer("-", "_") viper.SetEnvKeyReplacer(replacer) // viper precedence order // - explicit call to Set // - flag // - env // - config // - key/value store // - default // if a configfile has been passed, make viper read it if err = loadConfigFile(); err != nil { return nil, fmt.Errorf("Fatal error reading config file: %w", err) } // convert string of csv to String Slice if endpointsString := viper.GetString("dcs-endpoints"); endpointsString != "" && strings.Contains(endpointsString, ",") { viper.Set("dcs-endpoints", strings.Split(endpointsString, ",")) } setDefaults() if err = checkMandatory(); err != nil { return nil, err } conf := &Config{} if err = viper.Unmarshal(conf); err != nil { zap.L().Fatal("unable to decode viper config into config struct, %v", zap.Error(err)) } conf.initLogger() printSettings() return conf, nil } func (conf *Config) initLogger() { lcfg := zap.Config{ Level: zap.NewAtomicLevelAt(map[bool]zapcore.Level{ false: zap.InfoLevel, true: zap.DebugLevel}[conf.Verbose]), Development: false, Sampling: &zap.SamplingConfig{ Initial: 100, Thereafter: 100, }, Encoding: "console", // copied from "zap.NewProductionEncoderConfig" with some updates EncoderConfig: zapcore.EncoderConfig{ TimeKey: "ts", LevelKey: "level", NameKey: "logger", CallerKey: "caller", MessageKey: "msg", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalColorLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: map[bool]zapcore.CallerEncoder{ false: nil, true: zapcore.ShortCallerEncoder}[conf.Verbose], }, // Use "/dev/null" to discard all OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, } var err error conf.Logger, err = lcfg.Build() if err != nil { panic(err) } } vip-manager-4.0.0/vipconfig/vip-manager.yml000066400000000000000000000042431502154252400206410ustar00rootroot00000000000000# config for vip-manager by Cybertec Schönig & Schönig GmbH # time (in milliseconds) after which vip-manager wakes up and checks if it needs to register or release ip addresses. interval: 1000 # the etcd or consul key which vip-manager will regularly poll. trigger-key: "/service/pgcluster/leader" # if the value of the above key matches the trigger-value (often the hostname of this host), vip-manager will try to add the virtual ip address to the interface specified in Iface trigger-value: "pgcluster_member1" ip: 192.168.0.123 # the virtual ip address to manage netmask: 24 # netmask for the virtual ip interface: enp0s3 #interface to which the virtual ip will be added # how the virtual ip should be managed. we currently support "ip addr add/remove" through shell commands or the Hetzner api hosting-type: basic # possible values: basic, or hetzner. dcs-type: etcd # etcd, consul or patroni # a list that contains all DCS endpoints to which vip-manager could talk. dcs-endpoints: - http://127.0.0.1:2379 - https://192.168.0.42:2379 # A single list-item is also fine. # consul and patroni will always only use the first entry from this list. # For consul, you'll obviously need to change the port to 8500. Unless you're using a different one. Maybe you're a rebel and are running consul on port 2379? Just to confuse people? Why would you do that? Oh, I get it. etcd-user: "patroni" etcd-password: "Julian's secret password" # when etcd-ca-file is specified, TLS connections to the etcd endpoints will be used. etcd-ca-file: "/path/to/etcd/trusted/ca/file" # when etcd-cert-file and etcd-key-file are specified, we will authenticate at the etcd endpoints using this certificate and key. etcd-cert-file: "/path/to/etcd/client/cert/file" etcd-key-file: "/path/to/etcd/client/key/file" # don't worry about parameter with a prefix that doesn't match the endpoint_type. You can write anything there, I won't even look at it. consul-token: "Julian's secret token" # how often things should be retried and how long to wait between retries. (currently only affects arpClient) retry-num: 2 retry-after: 250 #in milliseconds # verbose logs (currently only supported for hetzner) verbose: false