diff --git a/.agola/config.jsonnet b/.agola/config.jsonnet deleted file mode 100644 index ce9443077..000000000 --- a/.agola/config.jsonnet +++ /dev/null @@ -1,208 +0,0 @@ -local go_runtime(version, arch) = { - type: 'pod', - arch: arch, - containers: [ - { - image: 'golang:' + version + '-buster', - }, - ], -}; - -local ci_runtime(pgversion, arch) = { - type: 'pod', - arch: arch, - containers: [ - { - image: 'sorintlab/stolon-ci-image:v0.2.0-pg' + pgversion, - volumes: [ - { - path: '/stolontemp', - tmpfs: {}, - }, - ], - }, - ], -}; - -local dind_runtime(arch) = { - type: 'pod', - arch: arch, - containers: [ - { - image: 'docker:stable-dind', - privileged: true, - entrypoint: 'dockerd --bip 172.18.0.1/16', - }, - ], -}; - -local task_build_go(version, arch) = { - name: 'build go ' + version + ' ' + arch, - runtime: go_runtime(version, arch), - environment: { - GO111MODULE: 'on', - }, - steps: [ - { type: 'clone' }, - { type: 'restore_cache', keys: ['cache-sum-{{ md5sum "go.sum" }}', 'cache-date-'], dest_dir: '/go/pkg/mod/cache' }, - { type: 'run', command: 'make' }, - { type: 'run', command: 'make test' }, - { type: 'run', name: 'build integration tests binary', command: 'go test -c ./tests/integration/ -o bin/integration-tests' }, - { type: 'save_cache', key: 'cache-sum-{{ md5sum "go.sum" }}', contents: [{ source_dir: '/go/pkg/mod/cache' }] }, - { type: 'save_cache', key: 'cache-date-{{ year }}-{{ month }}-{{ day }}', contents: [{ source_dir: '/go/pkg/mod/cache' }] }, - { type: 'save_to_workspace', contents: [{ source_dir: './bin', dest_dir: '/bin/', paths: ['*'] }] }, - ], -}; - -local task_integration_tests(store, pgversion, arch) = { - name: 'integration tests store: ' + store + ', postgres: ' + pgversion + ', arch: ' + arch, - runtime: ci_runtime(pgversion, 'amd64'), - environment: { - STOLON_TEST_STORE_BACKEND: store, - POSTGRES_PATH: '/usr/lib/postgresql/' + pgversion, - }, - steps: [ - { type: 'restore_workspace', dest_dir: '.' }, - { - type: 'run', - name: 'test', - command: ||| - export TMPDIR=/stolontemp - export PATH=$POSTGRES_PATH:$PATH - export BINDIR=${PWD}/bin - export STKEEPER_BIN=${BINDIR}/stolon-keeper - export STSENTINEL_BIN=${BINDIR}/stolon-sentinel - export STPROXY_BIN=${BINDIR}/stolon-proxy - export STCTL_BIN=${BINDIR}/stolonctl - export ETCD_BIN="${HOME}/etcd/etcd" - export CONSUL_BIN="${HOME}/consul" - INTEGRATION=1 ./bin/integration-tests -test.parallel 2 -test.v - |||, - }, - ], - depends: [ - 'build go 1.16 ' + arch, - ], -}; - -local task_build_push_images(name, pgversions, istag, push) = - local imagebase = if istag then 'sorintlab/stolon:${AGOLA_GIT_TAG:-test}' else 'sorintlab/stolon:${AGOLA_GIT_BRANCH:-test}'; - { - name: name, - runtime: dind_runtime('amd64'), - environment: { - DOCKERAUTH: { from_variable: 'dockerauth' }, - }, - working_dir: '/stolon', - steps: [ - { type: 'restore_workspace', dest_dir: '/stolon' }, - { type: 'run', command: 'apk add make' }, - ] + std.prune([ - if push then { - type: 'run', - name: 'generate docker auth', - command: ||| - mkdir ~/.docker - cat << EOF > ~/.docker/config.json - { - "auths": { - "https://index.docker.io/v1/": { "auth" : "$DOCKERAUTH" } - } - } - EOF - |||, - }, - ]) + [ - { type: 'run', command: 'for PGVERSION in %s; do make PGVERSION=${PGVERSION} TAG=%s-pg${PGVERSION} docker; done' % [pgversions, imagebase] }, - ] + std.prune([ - if push then { type: 'run', command: 'for PGVERSION in %s; do docker push %s-pg${PGVERSION}; done' % [pgversions, imagebase] }, - ]), - depends: ['checkout code and save to workspace', 'integration tests store: etcdv3, postgres: 11, arch: amd64'], - }; - -{ - runs: [ - { - name: 'stolon build/test', - tasks: [ - { - name: 'checkout code and save to workspace', - runtime: { - arch: 'amd64', - containers: [ - { - image: 'alpine/git', - }, - ], - }, - steps: [ - { type: 'clone' }, - { type: 'save_to_workspace', contents: [{ source_dir: '.', dest_dir: '.', paths: ['**'] }] }, - ], - depends: [], - }, - { - name: 'k8s test', - runtime: { - arch: 'amd64', - containers: [ - { - image: 'bsycorp/kind:latest-1.23', - privileged: true, - entrypoint: '/usr/bin/supervisord --nodaemon -c /etc/supervisord.conf', - }, - ], - }, - working_dir: '/stolon', - steps: [ - { type: 'restore_workspace', dest_dir: '/stolon' }, - { type: 'run', command: 'apk add bash' }, - { type: 'run', command: './scripts/agola-k8s.sh' }, - ], - depends: ['checkout code and save to workspace'], - }, - ] + std.flattenArrays([ - [ - task_build_go(version, arch), - ] - for version in ['1.15', '1.16'] - for arch in ['amd64' /*, 'arm64' */] - ]) + std.flattenArrays([ - [ - task_integration_tests(store, pgversion, 'amd64'), - ] - for store in ['etcdv2', 'consul'] - for pgversion in ['15'] - ]) + std.flattenArrays([ - [ - task_integration_tests(store, pgversion, 'amd64'), - ] - for store in ['etcdv3'] - for pgversion in ['10', '11', '12', '13', '14', '15'] - ]) + [ - task_build_push_images('test build docker "stolon" images', '10 11 12 13 14 15', false, false) - + { - when: { - branch: { - include: ['#.*#'], - exclude: ['master'], - }, - ref: '#refs/pull/\\d+/head#', - }, - }, - task_build_push_images('build and push docker "stolon" master branch images', '10 11 12 13 14 15', false, true) - + { - when: { - branch: 'master', - }, - }, - task_build_push_images('build and push docker "stolon" tag images', '10 11 12 13 14 15', true, true) - + { - when: { - tag: '#v.*#', - }, - }, - ], - }, - ], -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b38ba7961 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "fix" + prefix-development: "chore" + include: "scope" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "fix" + prefix-development: "chore" + include: "scope" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "fix" + prefix-development: "chore" + include: "scope" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "fix" + prefix-development: "chore" + include: "scope" diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json new file mode 100644 index 000000000..32446313d --- /dev/null +++ b/.github/pr-title-checker-config.json @@ -0,0 +1,16 @@ +{ + "LABEL": { + "name": "title needs formatting", + "color": "EEEEEE" + }, + "CHECKS": { + "prefixes": ["[Bot] docs: "], + "regexp": "^(feat|perf|fix|hotfix|bug|docs|test|revert|refactor|ci|build|chore)!?(\\(.*\\))?!?:.*" + }, + "MESSAGES": { + "success": "PR title is valid", + "failure": "PR title is invalid", + "notice": "PR Title needs to pass regex '^(feat|perf|fix|hotfix|bug|docs|test|revert|refactor|ci|build|chore)!?(\\(.*\\))?!?:.*" + } +} + diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 000000000..d70ff61f3 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,97 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +exclude-labels: + - 'skip-changelog' +replacers: + - search: '/CVE-(\d{4})-(\d+)/g' + replace: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-$1-$2' +categories: + - title: '💥 BREAKING CHANGES' + labels: + - 'breaking' + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + labels: + - 'chore' + - 'documentation' + - 'ci' + - 'refactor' + - 'style' + - 'test' + - title: '🔒 Security' + labels: + - 'dependencies' + - 'security' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + - 'breaking' + minor: + labels: + - 'minor' + - 'refactor' + - 'enhancement' + patch: + labels: + - 'patch' + - 'documentation' + - 'ci' + - 'style' + - 'test' + default: patch +autolabeler: + - label: 'breaking' + title: + - '/!:/' + body: + - '/BREAKING CHANGE/' + - label: 'chore' + title: + - '/^chore(\([a-z]*\))?!?:/i' + - label: 'ci' + title: + - '/^(ci|build)(\([a-z]*\))?!?:/i' + files: + - '.github/*' + - '.github/**/*' + - label: 'documentation' + title: + - '/^docs(\([a-z]*\))?!?:/i' + files: + - 'docs/*' + - 'docs/**/*' + - '*.md' + - '**/*.md' + - label: 'enhancement' + title: + - '/^(feat|perf)(\([a-z]*\))?!?:/i' + - label: 'bug' + title: + - '/^(fix|hotfix|bug)(\([a-z]*\))?!?:/i' + - label: 'refactor' + title: + - '/^refactor(\([a-z]*\))?!?:/i' + - label: 'revert' + title: + - '/^revert(\([a-z]*\))?!?:/i' + - label: 'style' + title: + - '/^style(\([a-z]*\))?!?:/i' + - label: 'test' + title: + - '/^test(\([a-z]*\))?!?:/i' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/build-and-release-binaries.yml b/.github/workflows/build-and-release-binaries.yml new file mode 100644 index 000000000..07e95f82b --- /dev/null +++ b/.github/workflows/build-and-release-binaries.yml @@ -0,0 +1,39 @@ +name: Build and add binaries to release +on: + release: + types: + - published + +env: + # Golang version to use across CI steps + GOLANG_VERSION: '1.26' # As we have go toolchain 1.26 specified in go.mod + +jobs: + binaries: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ env.GOLANG_VERSION }} + + - name: Download cyclonedx-gomod + uses: CycloneDX/gh-gomod-generate-sbom@v2 + with: + version: v1 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-and-release-images.yml b/.github/workflows/build-and-release-images.yml new file mode 100644 index 000000000..e3a82bd82 --- /dev/null +++ b/.github/workflows/build-and-release-images.yml @@ -0,0 +1,53 @@ +name: Build images and add to ghcr.io upon release +on: + release: + types: + - published + +jobs: + proxy: + name: Push proxy image to ghcr.io + runs-on: ubuntu-latest + permissions: + packages: write + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: stolon-meta + uses: docker/metadata-action@v5 + with: + images: | + name=ghcr.io/${{ github.repository }},enable=true + tags: | + type=semver,pattern={{raw}} + type=raw,value=latest + type=sha + + - name: Build and push stolon images + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile.proxy + push: true + platforms: 'linux/amd64,linux/arm64' + tags: ${{ steps.stolon-meta.outputs.tags }} + labels: ${{ steps.stolon-meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/build-and-release-sbom.yml b/.github/workflows/build-and-release-sbom.yml new file mode 100644 index 000000000..146590080 --- /dev/null +++ b/.github/workflows/build-and-release-sbom.yml @@ -0,0 +1,56 @@ +name: Build add SBOM and LICENSES.md to release +on: + release: + types: + - published + +jobs: + sbom: + name: Add files to release + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/amd64 + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate SBOM + uses: CycloneDX/gh-gomod-generate-sbom@v2 + with: + version: v1 + # added assert-licenses as required by dependency-track + args: mod -licenses -assert-licenses -output sources_${{ matrix.goos }}_${{ matrix.goarch }}.sbom.xml + env: + GOARCH: ${{ matrix.goarch }} + GOOS: ${{ matrix.goos }} + + - name: Generate LICENSES.md + if: ${{ matrix.goarch == 'amd64' && matrix.goos == 'linux' }} + uses: mvdkleijn/licenses-action@v1 + with: + # We only use the linux_amd64 variant here for generating the LICENSES.md + sbom: sources_linux_amd64.sbom.xml + type: xml + filename: LICENSES.md + template: | + # Licenses + + The following third-party licenses are applicable to this project: + + {{range .SortedKeys}}## {{.}} + + {{range index $.ComponentsByLicense .}}- {{.Name}} ({{.Version}}) + {{end}} + {{end}} + + - name: Add SBOM and LICENSES.md to release + uses: softprops/action-gh-release@v2 + with: + files: | + sources_${{ matrix.goos }}_${{ matrix.goarch }}.sbom.xml + LICENSES.md diff --git a/.github/workflows/ci-run-on-pr.yaml b/.github/workflows/ci-run-on-pr.yaml new file mode 100644 index 000000000..533c0ed86 --- /dev/null +++ b/.github/workflows/ci-run-on-pr.yaml @@ -0,0 +1,332 @@ +name: CI run on PR +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +env: + # Golang version to use across CI steps + GOLANG_VERSION: '1.25' # As we have go toolchain 1.25 specified in go.mod + DOWNLOAD_URL: https://github.com/etcd-io/etcd/releases/download + ETCD_VER: v3.6.8 + ETCD_PATH: /usr/local/bin/ + ETCD_BIN: /usr/local/bin/etcd + +permissions: + contents: read + +jobs: + codechanges: + runs-on: ubuntu-22.04 + outputs: + backend: ${{ steps.filter.outputs.backend_any_changed }} + ci: ${{ steps.filter.outputs.ci_any_changed }} + steps: + - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 # v45.0.1 + id: filter + with: + # Any file which is not under docs/, examples/, or is not a markdown file is counted as a backend file + # Also run when ci-run-on-pr has been changed to validate it is working + files_yaml: | + backend: + - '!**.md' + - '!**/*.md' + - '!docs/**' + - '!examples/**' + - '!.github/**' + ci: + - '.github/workflows/ci-run-on-pr.yaml' + + check-go: + name: Ensure Go modules synchronicity + runs-on: ubuntu-22.04 + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + steps: + - name: Checkout code + uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + - name: Setup Golang + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: ${{ env.GOLANG_VERSION }} + - name: Download all Go modules + run: | + go mod download + - name: Check for tidiness of go.mod and go.sum + run: | + go mod tidy + git diff --exit-code -- . + + lint-go: + name: Lint Go code + runs-on: ubuntu-22.04 + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.0.0 + - name: Setup Golang + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GOLANG_VERSION }} + - name: Run golangci-lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + with: + args: --verbose --timeout=10m + + shellcheck: + name: Shellcheck + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + + unit-test: + name: Run unit tests + runs-on: ubuntu-latest + needs: + - codechanges + permissions: + pull-requests: write + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.0.0 + - name: Setup Golang + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: ${{ env.GOLANG_VERSION }} + - name: Test + run: make test + - name: check test coverage + id: coverage + uses: vladopajic/go-test-coverage@v2 + with: + config: .testcoverage.yaml + profile: cover.out + continue-on-error: true # Should fail after coverage comment is posted + - name: Generate test results artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: unittests-coverage + path: cover.out + - name: find pull request ID + run: | + PR_ID=${GITHUB_REF_NAME%%/merge} + echo "PR_ID: $PR_ID" + + if [ "$PR_ID" != "null" ]; then + echo "pull_request_id=$PR_ID" >> $GITHUB_ENV + else + echo "No open pull request found for this branch." + fi + - name: create badge + uses: vladopajic/go-test-coverage@v2 + with: + profile: cover.out + threshold-total: 26 + git-token: ${{ github.ref_name == 'main' && secrets.GITHUB_TOKEN || '' }} + git-branch: badges + - name: post coverage report + if: env.pull_request_id + uses: thollander/actions-comment-pull-request@v3 + continue-on-error: true #we need pull_request_target or this will fail when not running pr in BD organisation + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-tag: coverage-report + pr-number: ${{ env.pull_request_id }} + message: | + go-test-coverage report: + ``` + ${{ fromJSON(steps.coverage.outputs.report) }}``` + - name: 'finally check coverage' + if: steps.coverage.outcome == 'failure' + shell: bash + run: echo "coverage check failed" && exit 1 + + e2e-keeper-images: + name: Build keeper images for running e2e + runs-on: ubuntu-22.04 + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + strategy: + matrix: + pgversion: ['14','15','16','17','18'] + steps: + - name: Checkout code + uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: stolon-meta + uses: docker/metadata-action@v5 + with: + images: | + name=keeper-${{ matrix.pgversion }},enable=true + tags: | + type=semver,pattern={{raw}} + type=raw,value=latest + type=sha + + - name: Build stolon images + uses: docker/build-push-action@v6 + with: + load: true + build-args: PGVERSION=${{ matrix.pgversion }} + context: . + file: ./Dockerfile.keeper + push: false + platforms: linux/amd64 + tags: ${{ steps.stolon-meta.outputs.tags }} + labels: ${{ steps.stolon-meta.outputs.labels }} + + - name: Save image to tar + run: docker save keeper-${{ matrix.pgversion }} > keeper-${{ matrix.pgversion }}.tar + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: keeper-image-${{ matrix.pgversion }} + path: keeper-${{ matrix.pgversion }}.tar + + e2e-other-images: + name: Build other images for running e2e + runs-on: ubuntu-22.04 + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + strategy: + matrix: + image: + - proxy + - sentinel + - stolonctl + steps: + - name: Checkout code + uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: stolon-meta + uses: docker/metadata-action@v5 + with: + images: | + name=${{ matrix.image }},enable=true + tags: | + type=semver,pattern={{raw}} + type=raw,value=latest + type=sha + + - name: Build stolon images + uses: docker/build-push-action@v6 + with: + load: true + context: . + file: ./Dockerfile.${{ matrix.image }} + push: false + platforms: linux/amd64 + tags: ${{ steps.stolon-meta.outputs.tags }} + labels: ${{ steps.stolon-meta.outputs.labels }} + + - name: Save image to tar + run: docker save ${{ matrix.image }} > ${{ matrix.image }}.tar + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.image }}-image + path: ${{ matrix.image }}.tar + + e2e-test: + name: Run e2e tests + runs-on: ubuntu-22.04 + needs: + - codechanges + - e2e-keeper-images + - e2e-other-images + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + strategy: + matrix: + pgversion: ['14','15','16','17','18'] + steps: + - name: Checkout code + uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + + - name: Setup Golang + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: ${{ env.GOLANG_VERSION }} + + - name: installe etcd + run: | + curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz + tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C "${ETCD_PATH}" --strip-components=1 --no-same-owner + rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz + + - name: Download keeper artifact + uses: actions/download-artifact@v4 + with: + path: images + merge-multiple: true + pattern: keeper-image-${{ matrix.pgversion }} + + - name: Download other artifacts + uses: actions/download-artifact@v4 + with: + path: images + merge-multiple: true + pattern: '*-image' + + - name: Load image + run: | + for f in images/*.tar; do + if [ -f "$f" ]; then + echo "Loading $f..." + docker load < "$f" + fi + done + + - name: Test + run: make e2e-test + env: + PGVERSION: ${{ matrix.pgversion }} + STOLON_TEST_STORE_BACKEND: etcdv3 + + license-header-test: + name: License header test + runs-on: ubuntu-22.04 + needs: + - codechanges + if: ${{ (needs.codechanges.outputs.backend == 'true' || needs.codechanges.outputs.ci == 'true') && github.event.pull_request.draft == false}} + steps: + - name: Checkout code + uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + - name: Test + run: | + function go_files_without_license_info() { + find . -type f -iname '*.go' ! -path './vendor/*' ! -path './tests/*' ! -name '*_test.go' | while read -r file; do + head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}" + done + } + + echo "Checking for license header..." + IFS=$'\n' read -r -d '' -a licRes < <(go_files_without_license_info) || true + if ((${#licRes[@]})); then + echo -e "license header checking failed:\n${licRes[*]}" + exit 255 + fi + + echo Success diff --git a/.github/workflows/draft-release-on-push.yml b/.github/workflows/draft-release-on-push.yml new file mode 100644 index 000000000..176cd2708 --- /dev/null +++ b/.github/workflows/draft-release-on-push.yml @@ -0,0 +1,38 @@ +name: Draft release on push to main + +on: + workflow_dispatch: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + issues: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v6 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 000000000..81c198192 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,29 @@ +name: "Lint PR" + +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + +# IMPORTANT: No checkout actions, scripts, or builds should be added to this workflow. Permissions should always be used +# with extreme caution. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target +permissions: {} + +# PR updates can happen in quick succession leading to this +# workflow being trigger a number of times. This limits it +# to one run per PR. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + validate: + permissions: + contents: read + pull-requests: read + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + configuration_path: ".github/pr-title-checker-config.json" diff --git a/.gitignore b/.gitignore index e543dc6a7..18a7e165f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ bin/ /release/ .idea +cover.out diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000..0b70e1d4e --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,112 @@ +--- +version: '2' +run: + allow-parallel-runners: true +linters: + default: none + enable: + - errcheck + #- ginkgolinter + #- gocyclo + #- govet + #- ineffassign + #- misspell + - revive + #- staticcheck + #- unused + settings: + govet: + enable: + - shadow + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + revive: + rules: + - name: add-constant + arguments: + - allowInts: 0,1,2,3 + allowStrs: '"","error: %v"' + ignoreFuncs: assert\.Len,require\.Len + maxLitCount: '5' + - name: line-length-limit + arguments: + - 120 + severity: warning + exclude: + - '' + - name: comment-spacings + - name: indent-error-flow + - name: use-errors-new + - name: bare-return + - name: cognitive-complexity + # TODO: wait for https://github.com/golangci/golangci-lint/pull/5663 + disabled: true + - name: context-as-argument + - name: cyclomatic + disabled: true + - name: dot-imports + arguments: + - allowedPackages: + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega + - name: early-return + - name: empty-block + - name: empty-lines + - name: exported + #- name: function-length + - name: if-return + - name: import-alias-naming + - name: import-shadowing + - name: increment-decrement + #- name: max-control-nesting + - name: max-public-structs + arguments: + - 14 + - name: redefines-builtin-id + - name: receiver-naming + - name: redundant-import-alias + - name: struct-tag + - name: superfluous-else + - name: unchecked-type-assertion + - name: unexported-naming + staticcheck: + checks: + - all + - '-ST1000' + - '-ST1003' + - '-ST1016' + - '-SA1019' + - '-ST1020' + - '-ST1021' + - '-ST1022' + exclusions: + generated: lax + rules: + - linters: + - dupl + path: internal/* + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - errcheck + - revive + - path: tests/ + linters: + - errcheck + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000..e61c12f6e --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,103 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +builds: + - id: stolon-keeper + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + main: ./cmd/keeper + binary: stolon-keeper + ldflags: + - -s -w -X "cmd.Version={{.Version}}" + - id: stolon-proxy + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + main: ./cmd/proxy + binary: stolon-proxy + ldflags: + - -s -w -X "cmd.Version={{.Version}}" + - id: stolon-sentinel + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + main: ./cmd/sentinel + binary: stolon-sentinel + ldflags: + - -s -w -X "cmd.Version={{.Version}}" + - id: stolonctl + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + main: ./cmd/stolonctl + binary: stolonctl + ldflags: + - -s -w -X "cmd.Version={{.Version}}" + +archives: + - formats: + - tar.gz + format_overrides: + - goos: windows + formats: + - zip + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: stolon_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }} + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# Please see https://github.com/CycloneDX/cyclonedx-gomod?tab=readme-ov-file#goreleaser- +sboms: + - artifacts: binary + documents: + - '{{ .Binary }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom.json' + cmd: cyclonedx-gomod + # changes: xml instead of json, and assert-licenses to add licen info per component + args: + [ + 'mod', + '-licenses', + '-assert-licenses', + '-std', + '-output', + '$document', + '../', + ] diff --git a/.testcoverage.yaml b/.testcoverage.yaml new file mode 100644 index 000000000..dd5882f45 --- /dev/null +++ b/.testcoverage.yaml @@ -0,0 +1,91 @@ +# (mandatory) +# Path to coverage profile file (output of `go test -coverprofile` command). +# +# For cases where there are many coverage profiles, such as when running +# unit tests and integration tests separately, you can combine all those +# profiles into one. In this case, the profile should have a comma-separated list +# of profile files, e.g., 'cover_unit.out,cover_integration.out'. +profile: cover.out + +# (optional; but recommended to set) +# When specified reported file paths will not contain local prefix in the output. +local-prefix: 'github.com/pgvillage-tools/stolon' + +# Holds coverage thresholds percentages, values should be in range [0-100]. +threshold: + # (optional; default 0) + # Minimum coverage percentage required for individual files. + file: 70 + + # (optional; default 0) + # Minimum coverage percentage required for each package. + package: 80 + + # (optional; default 0) + # Minimum overall project coverage percentage required. + #total: 80 + total: 29 + +# Holds regexp rules which will override thresholds for matched files or packages +# using their paths. +# +# First rule from this list that matches file or package is going to apply +# new threshold to it. If project has multiple rules that match same path, +# override rules should be listed in order from specific to more general rules. +override: + # Increase coverage threshold to 100% for `foo` package + # (default is 80, as configured above in this example). + #- path: ^pkg/lib/foo$ + # threshold: 100 + - path: ^cmd + + - path: ^internal/mock/store/store.go + threshold: 22 + - path: ^internal/postgresql/main.go + threshold: 25 + - path: ^internal/postgresql/manager.go + threshold: 0 + - path: ^internal/postgresql/recovery_options.go + threshold: 0 + - path: ^internal/flagutil/env.go + threshold: 0 + - path: ^internal/store/election.go + threshold: 0 + - path: ^internal/store/etcdv3.go + threshold: 0 + - path: ^internal/store/k8s.go + threshold: 0 + - path: ^internal/store/kvbacked.go + threshold: 0 + - path: ^internal/store/libkv.go + threshold: 0 + - path: scripts/gen_commands_doc.go + threshold: 0 + + - path: ^internal/mock/store$ + threshold: 22 + - path: ^internal/postgresql$ + threshold: 13.9 + - path: ^internal/flagutil$ + threshold: 0 + - path: ^internal/store$ + threshold: 0 + - path: ^scripts$ + threshold: 0 + +# Holds regexp rules which will exclude matched files or packages +# from coverage statistics. +exclude: + # Exclude files or packages matching their paths + paths: + - cmd + - e2e + +# File name of go-test-coverage breakdown file, which can be used to +# analyze coverage difference. +breakdown-file-name: '' + +diff: + # File name of go-test-coverage breakdown file which will be used to + # report coverage difference. + base-breakdown-file-name: '' diff --git a/Dockerfile.keeper b/Dockerfile.keeper new file mode 100644 index 000000000..cb469e266 --- /dev/null +++ b/Dockerfile.keeper @@ -0,0 +1,45 @@ +# Build the manager binary +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=v0.0.0-devel +ARG PGVERSION=18 + +FROM docker.io/golang:bookworm AS builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# Copy the go source +COPY api/ api/ +COPY cmd/ cmd/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -v -a -ldflags="-X 'cmd.Version=$VERSION'" -o stolon-keeper ./cmd/keeper + +FROM docker.io/postgres:${PGVERSION} +ARG PGVERSION=18 + +LABEL MAINTAINER=Nibble-IT +WORKDIR / +COPY --from=builder /workspace/stolon-keeper ./ + +RUN mkdir /pgdata /pgwal && chown postgres: /pgdata /pgwal + +ENV STKEEPER_PG_BIN_PATH=/usr/lib/postgresql/${PGVERSION}/bin \ + STKEEPER_CLUSTER_NAME=stolon \ + STKEEPER_STORE_BACKEND=etcdv3 \ + STKEEPER_DATA_DIR=/pgdata \ + STKEEPER_PGDATA_DIR=/pgdata/pgdata \ + STKEEPER_WAL_DIR=/pgwal/pgwal \ + STKEEPER_PG_PORT=5432 \ + STKEEPER_PG_REPL_USERNAME=postgres + +ENTRYPOINT ["/stolon-keeper"] +USER postgres diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 000000000..27f473312 --- /dev/null +++ b/Dockerfile.proxy @@ -0,0 +1,38 @@ +# Build the manager binary +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=v0.0.0-devel + +FROM docker.io/golang:bookworm AS builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# Copy the go source +COPY api/ api/ +COPY cmd/ cmd/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -v -a -ldflags="-X 'cmd.Version=$VERSION'" -o stolon-proxy ./cmd/proxy + +FROM docker.io/debian:bookworm-slim + +LABEL MAINTAINER=mannemsolutions +WORKDIR / +COPY --from=builder /workspace/stolon-proxy ./ + +ENV STPROXY_CLUSTER_NAME=stolon \ + STPROXY_STORE_BACKEND=etcdv3 \ + STPROXY_LISTEN_ADDRESS=0.0.0.0 \ + STPROXY_PORT=25432 + +EXPOSE 25432 + +ENTRYPOINT ["/stolon-proxy"] diff --git a/Dockerfile.sentinel b/Dockerfile.sentinel new file mode 100644 index 000000000..2858d28a0 --- /dev/null +++ b/Dockerfile.sentinel @@ -0,0 +1,34 @@ +# Build the manager binary +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=v0.0.0-devel + +FROM docker.io/golang:bookworm AS builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# Copy the go source +COPY api/ api/ +COPY cmd/ cmd/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -v -a -ldflags="-X 'cmd.Version=$VERSION'" -o stolon-sentinel ./cmd/sentinel + +FROM docker.io/debian:bookworm-slim + +LABEL MAINTAINER=mannemsolutions +WORKDIR / +COPY --from=builder /workspace/stolon-sentinel ./ + +ENV STSENTINEL_CLUSTER_NAME=stolon \ + STSENTINEL_STORE_BACKEND=etcdv3 + +ENTRYPOINT ["/stolon-sentinel"] diff --git a/Dockerfile.stolonctl b/Dockerfile.stolonctl new file mode 100644 index 000000000..81445a86c --- /dev/null +++ b/Dockerfile.stolonctl @@ -0,0 +1,34 @@ +# Build the manager binary +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=v0.0.0-devel + +FROM docker.io/golang:bookworm AS builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum + +# Copy the go source +COPY api/ api/ +COPY cmd/ cmd/ +COPY internal/ internal/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -v -a -ldflags="-X 'cmd.Version=$VERSION'" -o stolonctl ./cmd/stolonctl + +FROM docker.io/debian:bookworm-slim + +LABEL MAINTAINER=mannemsolutions +WORKDIR / +COPY --from=builder /workspace/stolonctl ./ + +ENV STOLONCTL_CLUSTER_NAME=stolon \ + STOLONCTL_STORE_BACKEND=etcdv3 + +ENTRYPOINT ["/stolonctl"] diff --git a/Makefile b/Makefile index 84dc91006..963d6dbbe 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ PROJDIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) # change to project dir so we can express all as relative paths $(shell cd $(PROJDIR)) -REPO_PATH=github.com/sorintlab/stolon +REPO_PATH=github.com/pgvillage-tools/stolon + +PGVERSION ?= 18 VERSION ?= $(shell scripts/git-version.sh) @@ -11,6 +13,11 @@ LD_FLAGS="-w -X $(REPO_PATH)/cmd.Version=$(VERSION)" $(shell mkdir -p bin ) +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker .PHONY: all all: build @@ -18,10 +25,6 @@ all: build .PHONY: build build: sentinel keeper proxy stolonctl -.PHONY: test -test: build - ./test - .PHONY: sentinel keeper proxy stolonctl docker keeper: @@ -38,6 +41,54 @@ stolonctl: .PHONY: docker docker: - if [ -z $${PGVERSION} ]; then echo 'PGVERSION is undefined'; exit 1; fi; \ if [ -z $${TAG} ]; then echo 'TAG is undefined'; exit 1; fi; \ docker build --build-arg PGVERSION=${PGVERSION} -t ${TAG} -f examples/kubernetes/image/docker/Dockerfile . + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +.PHONY: test +test: + go test $$(go list ./... | grep -v github.com/pgvillage-tools/stolon/tests) -coverprofile cover.out -coverpkg=./... + +.PHONY: install-go-test-coverage +install-go-test-coverage: + go install github.com/vladopajic/go-test-coverage/v2@latest + +.PHONY: check-coverage +check-coverage: install-go-test-coverage test + ${GOBIN}/go-test-coverage --config=./.testcoverage.yaml + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: build-images +build-images: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t keeper-$(PGVERSION) --build-arg PGVERSION=$(PGVERSION) -f Dockerfile.keeper . + $(CONTAINER_TOOL) build -t stolonctl -f Dockerfile.stolonctl . + $(CONTAINER_TOOL) build -t proxy -f Dockerfile.proxy . + $(CONTAINER_TOOL) build -t sentinel -f Dockerfile.sentinel . + +.PHONY: clean-e2e-containers +clean-e2e-containers: ## Clean containers for previous e2e runs + $(CONTAINER_TOOL) ps -a | grep -v CONTAINER | sed 's/.* //' | xargs $(CONTAINER_TOOL) stop + $(CONTAINER_TOOL) ps -a | grep -v CONTAINER | sed 's/.* //' | xargs $(CONTAINER_TOOL) rm + +.PHONY: clean-e2e-images +clean-e2e-images: ## Clean containers for previous e2e runs + $(CONTAINER_TOOL) images | awk '{if (length($$1)==54)print $$1":"$$2}' | xargs $(CONTAINER_TOOL) rmi + $(CONTAINER_TOOL) images | awk '//{print $$3}' | xargs $(CONTAINER_TOOL) rmi + +.PHONY: fast-e2e-test +fast-e2e-test: + cd ./tests/etcdv3 && PGVERSION=$(PGVERSION) go test -count=1 -v ./... + +.PHONY: full-e2e-test +full-e2e-test: build-images clean-e2e-containers clean-images fast-e2e-test + +.PHONY: e2e-test +e2e-test: fast-e2e-test diff --git a/internal/cluster/v0/clusterview.go b/api/v0/clusterview.go similarity index 74% rename from internal/cluster/v0/clusterview.go rename to api/v0/clusterview.go index b2529bb9d..3a549c766 100644 --- a/internal/cluster/v0/clusterview.go +++ b/api/v0/clusterview.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package v0 holds the v0 api of the cluster info package v0 import ( @@ -22,11 +24,14 @@ import ( ) const ( + // CurrentCDFormatVersion can be used to get the current version of a clusterdata CurrentCDFormatVersion uint64 = 0 ) +// KeepersState holds the state of all keepers of this cluster type KeepersState map[string]*KeeperState +// SortedKeys returns a sorted list of all keys of a KeepersState func (kss KeepersState) SortedKeys() []string { keys := []string{} for k := range kss { @@ -36,7 +41,11 @@ func (kss KeepersState) SortedKeys() []string { return keys } +// Copy will return a copy of a KeepersState func (kss KeepersState) Copy() KeepersState { + if kss == nil { + return nil + } nkss := KeepersState{} for k, v := range kss { nkss[k] = v.Copy() @@ -44,6 +53,7 @@ func (kss KeepersState) Copy() KeepersState { return nkss } +// NewFromKeeperInfo Initializes a KeoerInfo from a state func (kss KeepersState) NewFromKeeperInfo(ki *KeeperInfo) error { id := ki.ID if _, ok := kss[id]; ok { @@ -61,6 +71,7 @@ func (kss KeepersState) NewFromKeeperInfo(ki *KeeperInfo) error { return nil } +// KeeperState holds the state of one Keeper type KeeperState struct { ID string ErrorStartTime time.Time @@ -73,6 +84,7 @@ type KeeperState struct { PGState *PostgresState } +// Copy returns a copy of a KeeprSate func (ks *KeeperState) Copy() *KeeperState { if ks == nil { return nil @@ -81,6 +93,7 @@ func (ks *KeeperState) Copy() *KeeperState { return &nks } +// ChangedFromKeeperInfo returns true if a KeeperState has changed since it was defined for a Keeperinfo func (ks *KeeperState) ChangedFromKeeperInfo(ki *KeeperInfo) (bool, error) { if ks.ID != ki.ID { return false, fmt.Errorf("different IDs, keeperState.ID: %s != keeperInfo.ID: %s", ks.ID, ki.ID) @@ -95,6 +108,7 @@ func (ks *KeeperState) ChangedFromKeeperInfo(ki *KeeperInfo) (bool, error) { return false, nil } +// UpdateFromKeeperInfo will update a KeeperState from a KeeperInfo func (ks *KeeperState) UpdateFromKeeperInfo(ki *KeeperInfo) error { if ks.ID != ki.ID { return fmt.Errorf("different IDs, keeperState.ID: %s != keeperInfo.ID: %s", ks.ID, ki.ID) @@ -108,23 +122,43 @@ func (ks *KeeperState) UpdateFromKeeperInfo(ki *KeeperInfo) error { return nil } +// SetError will set ErrorStartTime Which defines that an error has occurred and also when func (ks *KeeperState) SetError() { if ks.ErrorStartTime.IsZero() { ks.ErrorStartTime = time.Now() } } +// CleanError will clear ErrorStartTime func (ks *KeeperState) CleanError() { ks.ErrorStartTime = time.Time{} } +// KeepersRole is a map of KepperRole instances type KeepersRole map[string]*KeeperRole +// GetFollowersIDs returns a sorted list of followersIDs +func (ksr KeepersRole) GetFollowersIDs(id string) []string { + followersIDs := []string{} + for keeperID, kr := range ksr { + if kr.Follow == id { + followersIDs = append(followersIDs, keeperID) + } + } + sort.Strings(followersIDs) + return followersIDs +} + +// NewKeepersRole returns a new KeepersRole func NewKeepersRole() KeepersRole { - return make(KeepersRole) + return KeepersRole{} } +// Copy returns a copy of a KeepersRole func (ksr KeepersRole) Copy() KeepersRole { + if ksr == nil { + return nil + } nksr := KeepersRole{} for k, v := range ksr { nksr[k] = v.Copy() @@ -132,6 +166,7 @@ func (ksr KeepersRole) Copy() KeepersRole { return nksr } +// Add is a safe method to add a role to a KeepersRole. It errors when it was already there func (ksr KeepersRole) Add(id string, follow string) error { if _, ok := ksr[id]; ok { return fmt.Errorf("keeperRole with id %q already exists", id) @@ -140,11 +175,13 @@ func (ksr KeepersRole) Add(id string, follow string) error { return nil } +// KeeperRole defines a role of a keeper, and what other keeper is is following type KeeperRole struct { ID string Follow string } +// Copy returns a copy of a KeeperRole func (kr *KeeperRole) Copy() *KeeperRole { if kr == nil { return nil @@ -153,11 +190,13 @@ func (kr *KeeperRole) Copy() *KeeperRole { return &nkr } +// ProxyConf defines a proxy configuration which consists of a host and port type ProxyConf struct { Host string Port string } +// Copy returns a copy of a ProxyConf func (pc *ProxyConf) Copy() *ProxyConf { if pc == nil { return nil @@ -166,6 +205,7 @@ func (pc *ProxyConf) Copy() *ProxyConf { return &npc } +// ClusterView defines an overview of a cluster type ClusterView struct { Version int Master string @@ -196,6 +236,7 @@ func (cv *ClusterView) Equals(ncv *ClusterView) bool { reflect.DeepEqual(cv.Config, ncv.Config) } +// Copy returns a copy of a ClusterView func (cv *ClusterView) Copy() *ClusterView { if cv == nil { return nil @@ -208,19 +249,12 @@ func (cv *ClusterView) Copy() *ClusterView { return &ncv } -// Returns a sorted list of followersIDs +// GetFollowersIDs returns a sorted list of followersIDs func (cv *ClusterView) GetFollowersIDs(id string) []string { - followersIDs := []string{} - for keeperID, kr := range cv.KeepersRole { - if kr.Follow == id { - followersIDs = append(followersIDs, keeperID) - } - } - sort.Strings(followersIDs) - return followersIDs + return cv.KeepersRole.GetFollowersIDs(id) } -// A struct containing the KeepersState and the ClusterView since they need to be in sync +// ClusterData defines a struct containing the KeepersState and the ClusterView since they need to be in sync type ClusterData struct { // ClusterData format version. Used to detect incompatible // version and do upgrade. Needs to be bumped when a non diff --git a/api/v0/clusterview_test.go b/api/v0/clusterview_test.go new file mode 100644 index 000000000..d75ff3ce7 --- /dev/null +++ b/api/v0/clusterview_test.go @@ -0,0 +1,284 @@ +package v0 + +import ( + "sort" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Clusterview", func() { + When("Copying a KeepersState", func() { + var ( + orgKss, newKss KeepersState + ) + BeforeEach(func() { + orgKss = randomKeepersState(5) + newKss = orgKss.Copy() + }) + It("should be a copy", func() { + for key := range newKss { + newKss[key].ClusterViewVersion = randomInt() + } + Ω(newKss).NotTo(Equal(orgKss)) + }) + It("should copy all fields", func() { + Ω(newKss).To(Equal(orgKss)) + }) + It("should return nil when KeepersState is nil", func() { + var kss KeepersState + Ω(kss.Copy()).To(BeNil()) + }) + }) + When("Copying a KeeperState", func() { + var ( + orgKS, newKS *KeeperState + ) + BeforeEach(func() { + orgKS = randomKeeperState() + newKS = orgKS.Copy() + }) + It("should be a copy", func() { + newKS.ClusterViewVersion = randomInt() + Ω(newKS).NotTo(Equal(orgKS)) + }) + It("should copy all fields", func() { + Ω(newKS).To(Equal(orgKS)) + }) + It("should return nil when KeeperState is nil", func() { + var ki *KeeperState + Ω(ki.Copy()).To(BeNil()) + }) + It("should return empty struct when KeeperState is empty struct", func() { + var ki KeeperState + Ω(ki.Copy()).To(Equal(&KeeperState{})) + }) + }) + When("Copying a KeepersRole", func() { + var ( + orgKsr, newKsr KeepersRole + ) + BeforeEach(func() { + orgKsr = randomKeepersRole(5) + newKsr = orgKsr.Copy() + }) + It("should be a copy", func() { + for key := range newKsr { + newKsr[key].Follow = randomString() + } + Ω(newKsr).NotTo(Equal(orgKsr)) + }) + It("should copy all fields", func() { + Ω(newKsr).To(Equal(orgKsr)) + }) + It("should return nil when KeepersRole is nil", func() { + var kss KeepersRole + Ω(kss.Copy()).To(BeNil()) + }) + }) + When("Copying a KeeperRole", func() { + var ( + orgKR, newKR *KeeperRole + ) + BeforeEach(func() { + orgKR = randomKeeperRole() + newKR = orgKR.Copy() + }) + It("should be a copy", func() { + newKR.Follow = randomString() + Ω(newKR).NotTo(Equal(orgKR)) + }) + It("should copy all fields", func() { + Ω(newKR).To(Equal(orgKR)) + }) + It("should return nil when KeeperState is nil", func() { + var ki *KeeperRole + Ω(ki.Copy()).To(BeNil()) + }) + It("should return empty struct when KeeperState is empty struct", func() { + var ki KeeperRole + Ω(ki.Copy()).To(Equal(&KeeperRole{})) + }) + }) + When("Copying a ProxyConf", func() { + var ( + orgPC, newPC *ProxyConf + ) + BeforeEach(func() { + orgPC = randomProxyConf() + newPC = orgPC.Copy() + }) + It("should be a copy", func() { + newPC.Port = randomPort() + Ω(newPC).NotTo(Equal(orgPC)) + }) + It("should copy all fields", func() { + Ω(newPC).To(Equal(orgPC)) + }) + It("should return nil when ProxyConf is nil", func() { + var ki *ProxyConf + Ω(ki.Copy()).To(BeNil()) + }) + It("should return empty struct when ProxyConf is empty struct", func() { + var ki ProxyConf + Ω(ki.Copy()).To(Equal(&ProxyConf{})) + }) + }) + When("Copying a ProxyConf", func() { + var ( + orgCV, newCV *ClusterView + ) + BeforeEach(func() { + orgCV = randomClusterView() + newCV = orgCV.Copy() + }) + It("should be a copy", func() { + newCV.Master = randomString() + Ω(newCV).NotTo(Equal(orgCV)) + Ω(newCV.Equals(orgCV)).NotTo(BeTrue()) + }) + It("should copy all fields", func() { + Ω(newCV).To(Equal(orgCV)) + Ω(newCV.Equals(orgCV)).To(BeTrue()) + }) + It("should return nil when ClusterView is nil", func() { + var ki *ClusterView + Ω(ki.Copy()).To(BeNil()) + }) + It("should return empty struct when ClusterView is empty struct", func() { + var ki ClusterView + Ω(ki.Copy()).To(Equal(&ClusterView{})) + }) + }) + When("Requesting sorted keys from KeepersState", func() { + var ( + orgKss KeepersState + sortedKeys []string + ) + BeforeEach(func() { + orgKss = randomKeepersState(20) + sortedKeys = orgKss.SortedKeys() + }) + It("should have same length as keepersState", func() { + Ω(sortedKeys).To(HaveLen(len(orgKss))) + }) + It("should contain all keys", func() { + for key := range orgKss { + Ω(sortedKeys).To(ContainElement(key)) + } + }) + It("should be sorted", func() { + sortedList := make([]string, len(sortedKeys)) + copy(sortedList, sortedKeys) + sort.Strings(sortedList) + Ω(sortedKeys).To(Equal(sortedList)) + }) + }) + When("Adding new KeeperState to KeepersState from KeeperInfo", Ordered, func() { + var ( + newInfo = randomKeeperInfo() + states = randomKeepersState(5) + ) + It("should properly generate and add a KeeperState if it is not in yet", func() { + Ω(states.NewFromKeeperInfo(newInfo)).NotTo(HaveOccurred()) + }) + It("should raise error if info is added a second time", func() { + Ω(states.NewFromKeeperInfo(newInfo)).To(HaveOccurred()) + }) + }) + When("Checking if a KeeperState has changed", func() { + var ( + i KeeperInfo + s KeeperState + ) + BeforeEach(func() { + i = *randomKeeperInfo() + s = KeeperState{ID: i.ID} + Ω(s.UpdateFromKeeperInfo(&i)).NotTo(HaveOccurred()) + }) + AfterEach(func() { + Ω(s.UpdateFromKeeperInfo(&i)).NotTo(HaveOccurred()) + Ω(s.ChangedFromKeeperInfo(&i)).NotTo(BeTrue()) + }) + It("should return true when ClusterViewVersion is changed", func() { + s.ClusterViewVersion = randomInt() + Ω(s.ChangedFromKeeperInfo(&i)).To(BeTrue()) + }) + It("should return true when ListenAddress is changed", func() { + s.ListenAddress = randomIP() + Ω(s.ChangedFromKeeperInfo(&i)).To(BeTrue()) + }) + It("should return true when Port is changed", func() { + s.Port = randomPort() + Ω(s.ChangedFromKeeperInfo(&i)).To(BeTrue()) + }) + It("should return true when PGListenAddress is changed", func() { + s.PGListenAddress = randomIP() + Ω(s.ChangedFromKeeperInfo(&i)).To(BeTrue()) + }) + It("should return true when PGPort is changed", func() { + s.PGPort = randomPort() + Ω(s.ChangedFromKeeperInfo(&i)).To(BeTrue()) + }) + }) + When("Working with KeeperState with errors", func() { + var ( + s KeeperState + ) + BeforeEach(func() { + s = *randomKeeperState() + }) + It("should properly set error", func() { + s.SetError() + Ω(s.ErrorStartTime.IsZero()).To(BeFalse()) + }) + It("should properly reset error", func() { + s.CleanError() + Ω(s.ErrorStartTime.IsZero()).To(BeTrue()) + }) + }) + When("Working with KeepersRole", func() { + var ( + r KeepersRole + ) + BeforeEach(func() { + r = NewKeepersRole() + }) + It("should be empty on initialization", func() { + Ω(r).To(HaveLen(0)) + }) + It("should be empty on initialization", func() { + var ( + id = randomString() + follow = randomString() + newKR = &KeeperRole{ID: id, Follow: follow} + ) + r.Add(id, follow) + Ω(r).To(HaveLen(1)) + Ω(r).To(HaveKeyWithValue(id, newKR)) + }) + }) + When("Working with a ClusterView", func() { + It("should successfully parse", func() { + }) + }) + /* + NewClusterView() + ClusterView.Equals() + */ + + When("Creating getting followers ids from a ClusterView", func() { + var ( + v ClusterView + ) + BeforeEach(func() { + v = *randomClusterView() + }) + It("should properly work as expected", func() { + for _, role := range v.KeepersRole { + fIDs := v.GetFollowersIDs(role.Follow) + Ω(fIDs).To(HaveLen(1)) + } + }) + }) +}) diff --git a/internal/cluster/v0/config.go b/api/v0/config.go similarity index 61% rename from internal/cluster/v0/config.go rename to api/v0/config.go index c3f070de7..f875bbfb8 100644 --- a/internal/cluster/v0/config.go +++ b/api/v0/config.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,23 +17,41 @@ package v0 import ( "encoding/json" + "errors" "fmt" - "strings" "time" + + "github.com/pgvillage-tools/stolon/internal/util" ) const ( + // DefaultProxyCheckInterval is the default for the interval for the proxy to check the endpoint DefaultProxyCheckInterval = 5 * time.Second - DefaultRequestTimeout = 10 * time.Second - DefaultSleepInterval = 5 * time.Second - DefaultKeeperFailInterval = 20 * time.Second - DefaultMaxStandbysPerSender = 3 - DefaultSynchronousReplication = false + // DefaultRequestTimeout is the default for a request to time out + DefaultRequestTimeout = 10 * time.Second + + // DefaultSleepInterval is the default for sleeps during checks + DefaultSleepInterval = 5 * time.Second + + // DefaultKeeperFailInterval sets the default for the keeper to be assumed unhealthy + DefaultKeeperFailInterval = 20 * time.Second + + // DefaultMaxStandbysPerSender sets the default for number of standby's before cleanup + // of old standby's is triggered. + DefaultMaxStandbysPerSender uint = 3 + + // DefaultSynchronousReplication sets the default for sync replication when not set by config + DefaultSynchronousReplication = false + + // DefaultInitWithMultipleKeepers can be set to choose a random initial master when multiple keeper are registered DefaultInitWithMultipleKeepers = false - DefaultUsePGRewind = false + + // DefaultUsePGRewind sets the default for using PgRewind (over starting over) + DefaultUsePGRewind = false ) +// NilConfig defines an empty config type NilConfig struct { RequestTimeout *Duration `json:"request_timeout,omitempty"` SleepInterval *Duration `json:"sleep_interval,omitempty"` @@ -44,6 +63,7 @@ type NilConfig struct { PGParameters *map[string]string `json:"pg_parameters,omitempty"` } +// Config defines the end result config type Config struct { // Time after which any request (keepers checks from sentinel etc...) will fail. RequestTimeout time.Duration @@ -64,76 +84,60 @@ type Config struct { PGParameters map[string]string } -func StringP(s string) *string { - return &s -} - -func UintP(u uint) *uint { - return &u -} - -func BoolP(b bool) *bool { - return &b -} - -func DurationP(d Duration) *Duration { - return &d -} - -func MapStringP(m map[string]string) *map[string]string { - nm := map[string]string{} - for k, v := range m { - nm[k] = v - } - return &nm -} - -type nilConfig NilConfig +// TODO: use json annotations instead of UnmarshalJSON +// UnmarshalJSON deserializes a NilConfig from JSON func (c *NilConfig) UnmarshalJSON(in []byte) error { - var nc nilConfig - if err := json.Unmarshal(in, &nc); err != nil { + type Alias NilConfig + aux := (*Alias)(c) + if err := json.Unmarshal(in, aux); err != nil { return err } - *c = NilConfig(nc) if err := c.Validate(); err != nil { return fmt.Errorf("config validation failed: %v", err) } return nil } +// Copy returns a shallow copy of a NilConfig func (c *NilConfig) Copy() *NilConfig { if c == nil { return c } var nc NilConfig if c.RequestTimeout != nil { - nc.RequestTimeout = DurationP(*c.RequestTimeout) + nc.RequestTimeout = &Duration{c.RequestTimeout.Duration} } if c.SleepInterval != nil { - nc.SleepInterval = DurationP(*c.SleepInterval) + nc.SleepInterval = &Duration{c.SleepInterval.Duration} } if c.KeeperFailInterval != nil { - nc.KeeperFailInterval = DurationP(*c.KeeperFailInterval) + nc.KeeperFailInterval = &Duration{c.KeeperFailInterval.Duration} } if c.MaxStandbysPerSender != nil { - nc.MaxStandbysPerSender = UintP(*c.MaxStandbysPerSender) + mspr := *c.MaxStandbysPerSender + nc.MaxStandbysPerSender = &mspr } if c.SynchronousReplication != nil { - nc.SynchronousReplication = BoolP(*c.SynchronousReplication) + sr := *c.SynchronousReplication + nc.SynchronousReplication = &sr } if c.InitWithMultipleKeepers != nil { - nc.InitWithMultipleKeepers = BoolP(*c.InitWithMultipleKeepers) + iwmk := *c.InitWithMultipleKeepers + nc.InitWithMultipleKeepers = &iwmk } if c.UsePGRewind != nil { - nc.UsePGRewind = BoolP(*c.UsePGRewind) + upgr := *c.UsePGRewind + nc.UsePGRewind = &upgr } if c.PGParameters != nil { - nc.PGParameters = MapStringP(*c.PGParameters) + pgp := *c.PGParameters + nc.PGParameters = &pgp } return &nc } +// Copy returns a shallow copy of a Config func (c *Config) Copy() *Config { if c == nil { return c @@ -155,36 +159,54 @@ type Duration struct { time.Duration } +// TODO convert MarshalJSON and UnmarshalJSON to using json annotations + +// MarshalJSON will serialize a Duration object func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) } +// UnmarshalJSON will deserialize a Duration object func (d *Duration) UnmarshalJSON(b []byte) error { - s := strings.Trim(string(b), `"`) - du, err := time.ParseDuration(s) - if err != nil { + var v any + if err := json.Unmarshal(b, &v); err != nil { return err } - d.Duration = du - return nil + + switch value := v.(type) { + case float64: + d.Duration = time.Duration(value) + return nil + case string: + var err error + d.Duration, err = time.ParseDuration(value) + if err != nil { + return err + } + return nil + default: + return errors.New("invalid duration") + } } +// Validate can be used to validate a NilConfig func (c *NilConfig) Validate() error { if c.RequestTimeout != nil && (*c.RequestTimeout).Duration < 0 { - return fmt.Errorf("request_timeout must be positive") + return errors.New("request_timeout must be positive") } if c.SleepInterval != nil && (*c.SleepInterval).Duration < 0 { - return fmt.Errorf("sleep_interval must be positive") + return errors.New("sleep_interval must be positive") } if c.KeeperFailInterval != nil && (*c.KeeperFailInterval).Duration < 0 { - return fmt.Errorf("keeper_fail_interval must be positive") + return errors.New("keeper_fail_interval must be positive") } if c.MaxStandbysPerSender != nil && *c.MaxStandbysPerSender < 1 { - return fmt.Errorf("max_standbys_per_sender must be at least 1") + return errors.New("max_standbys_per_sender must be at least 1") } return nil } +// MergeDefaults can be used to merge in Default values func (c *NilConfig) MergeDefaults() { if c.RequestTimeout == nil { c.RequestTimeout = &Duration{DefaultRequestTimeout} @@ -196,22 +218,23 @@ func (c *NilConfig) MergeDefaults() { c.KeeperFailInterval = &Duration{DefaultKeeperFailInterval} } if c.MaxStandbysPerSender == nil { - c.MaxStandbysPerSender = UintP(DefaultMaxStandbysPerSender) + c.MaxStandbysPerSender = util.ToPtr(DefaultMaxStandbysPerSender) } if c.SynchronousReplication == nil { - c.SynchronousReplication = BoolP(DefaultSynchronousReplication) + c.SynchronousReplication = util.ToPtr(DefaultSynchronousReplication) } if c.InitWithMultipleKeepers == nil { - c.InitWithMultipleKeepers = BoolP(DefaultInitWithMultipleKeepers) + c.InitWithMultipleKeepers = util.ToPtr(DefaultInitWithMultipleKeepers) } if c.UsePGRewind == nil { - c.UsePGRewind = BoolP(DefaultUsePGRewind) + c.UsePGRewind = util.ToPtr(DefaultUsePGRewind) } if c.PGParameters == nil { c.PGParameters = &map[string]string{} } } +// ToConfig converts a NilCOnfig into a Config func (c *NilConfig) ToConfig() *Config { nc := c.Copy() nc.MergeDefaults() @@ -227,6 +250,7 @@ func (c *NilConfig) ToConfig() *Config { } } +// NewDefaultConfig returns a freshly initialized config func NewDefaultConfig() *Config { nc := &NilConfig{} nc.MergeDefaults() diff --git a/api/v0/config_test.go b/api/v0/config_test.go new file mode 100644 index 000000000..da9de12b4 --- /dev/null +++ b/api/v0/config_test.go @@ -0,0 +1,174 @@ +package v0 + +import ( + "encoding/json" + "errors" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pgvillage-tools/stolon/internal/util" +) + +var _ = Describe("Config", func() { + var ( + tenSeconds = Duration{10 * time.Second} + hundredSeconds = Duration{100 * time.Second} + + mergeDefaults = func(c *NilConfig) *NilConfig { + c.MergeDefaults() + return c + } + ) + When("Parsing config", func() { + It("should successfully parse", func() { + const ( + defaultMaxStandbysPerSender uint = 10 + ) + + for _, tt := range []struct { + in string + cfg *Config + err error + }{ + { + in: "{}", + cfg: mergeDefaults(&NilConfig{}).ToConfig(), + err: nil, + }, + // Test duration parsing + { + in: `{ "request_timeout": "3s" }`, + cfg: mergeDefaults(&NilConfig{ + RequestTimeout: &Duration{3 * time.Second}}, + ).ToConfig(), + err: nil, + }, + { + in: `{ "request_timeout": "3000ms" }`, + cfg: mergeDefaults(&NilConfig{ + RequestTimeout: &Duration{3 * time.Second}}, + ).ToConfig(), + err: nil, + }, + { + in: `{ "request_timeout": "-3s" }`, + cfg: nil, + err: errors.New("config validation failed: request_timeout must be positive"), + }, + { + in: `{ "request_timeout": "-3s" }`, + cfg: nil, + err: errors.New("config validation failed: request_timeout must be positive"), + }, + { + in: `{ "sleep_interval": "-3s" }`, + cfg: nil, + err: errors.New("config validation failed: sleep_interval must be positive"), + }, + { + in: `{ "keeper_fail_interval": "-3s" }`, + cfg: nil, + err: errors.New("config validation failed: keeper_fail_interval must be positive"), + }, + { + in: `{ "max_standbys_per_sender": 0 }`, + cfg: nil, + err: errors.New("config validation failed: max_standbys_per_sender must be at least 1"), + }, + // All options defined + { + in: strings.Join([]string{ + `{ "request_timeout": "10s", `, + `"sleep_interval": "10s", `, + `"keeper_fail_interval": "100s", `, + `"max_standbys_per_sender": 5, `, + `"synchronous_replication": true, `, + `"init_with_multiple_keepers": true,`, + `"pg_parameters": {`, + ` "param01": "value01"`, + `}}`, + }, "\n"), + cfg: mergeDefaults(&NilConfig{ + RequestTimeout: &tenSeconds, + SleepInterval: &tenSeconds, + KeeperFailInterval: &hundredSeconds, + MaxStandbysPerSender: util.ToPtr(uint(5)), + SynchronousReplication: util.ToPtr(true), + InitWithMultipleKeepers: util.ToPtr(true), + PGParameters: &map[string]string{ + "param01": "value01", + }, + }).ToConfig(), + err: nil, + }, + } { + var nilCfg *NilConfig + err := json.Unmarshal([]byte(tt.in), &nilCfg) + if tt.err != nil { + Ω(err).NotTo(BeNil()) + Ω(tt.err.Error()).To(Equal(err.Error())) + } else { + Ω(err).To(BeNil()) + nilCfg.MergeDefaults() + Ω(nilCfg.ToConfig()).To(Equal(tt.cfg)) + } + } + }) + }) + When("Copying a nilConfig", func() { + const ( + defaultMaxStandbysPerSender uint = 10 + ) + var ( + defaultReqTimeout = Duration{5 * time.Second} + defaultInterval = Duration{6 * time.Second} + defaultMaxStbys = uint(5) + defaultSyncReplicas = true + defaultInitMultipleKeepers = true + + newReqTimeout = Duration{10 * time.Second} + newInterval = Duration{12 * time.Second} + newMaxStbys = uint(5) + newSyncReplicas = true + newInitMultipleKeepers = true + + cfg = mergeDefaults(&NilConfig{ + RequestTimeout: &defaultReqTimeout, + SleepInterval: &defaultInterval, + KeeperFailInterval: &defaultInterval, + MaxStandbysPerSender: &defaultMaxStbys, + SynchronousReplication: &defaultSyncReplicas, + InitWithMultipleKeepers: &defaultInitMultipleKeepers, + PGParameters: &map[string]string{ + "param01": "value01", + }, + }) + ) + It("changes on copy should not impact original config", func() { + newCfg := cfg.Copy() + newCfg.RequestTimeout = &newReqTimeout + newCfg.SleepInterval = &newInterval + newCfg.KeeperFailInterval = &newInterval + newCfg.MaxStandbysPerSender = &newMaxStbys + newCfg.SynchronousReplication = &newSyncReplicas + newCfg.InitWithMultipleKeepers = &newInitMultipleKeepers + (*newCfg.PGParameters)["param01"] = "anothervalue01" + + Ω(newCfg.RequestTimeout).To(Equal(&newReqTimeout)) + Ω(newCfg.SleepInterval).To(Equal(&newInterval)) + Ω(newCfg.KeeperFailInterval).To(Equal(&newInterval)) + Ω(newCfg.MaxStandbysPerSender).To(Equal(&newMaxStbys)) + Ω(newCfg.SynchronousReplication).To(Equal(&newSyncReplicas)) + Ω(newCfg.InitWithMultipleKeepers).To(Equal(&newInitMultipleKeepers)) + + Ω(cfg.RequestTimeout).To(Equal(&defaultReqTimeout)) + Ω(cfg.SleepInterval).To(Equal(&defaultInterval)) + Ω(cfg.KeeperFailInterval).To(Equal(&defaultInterval)) + Ω(cfg.MaxStandbysPerSender).To(Equal(&defaultMaxStbys)) + Ω(cfg.SynchronousReplication).To(Equal(&defaultSyncReplicas)) + Ω(cfg.InitWithMultipleKeepers).To(Equal(&defaultInitMultipleKeepers)) + }) + }) +}) diff --git a/internal/cluster/v0/member.go b/api/v0/member.go similarity index 67% rename from internal/cluster/v0/member.go rename to api/v0/member.go index 3e87ecd4d..28c1da41e 100644 --- a/internal/cluster/v0/member.go +++ b/api/v0/member.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,14 @@ package v0 -import "github.com/sorintlab/stolon/internal/common" +// TODO: Remove duplication with internal/cluster/member.go +import "github.com/pgvillage-tools/stolon/internal/common" + +// KeepersInfo stores all info on all keepers belonging to this cluster type KeepersInfo map[string]*KeeperInfo +// KeeperInfo stores all info on one keeper type KeeperInfo struct { ID string ClusterViewVersion int @@ -27,6 +32,7 @@ type KeeperInfo struct { PGPort string } +// Copy returns a shallow copy func (k *KeeperInfo) Copy() *KeeperInfo { if k == nil { return nil @@ -35,23 +41,33 @@ func (k *KeeperInfo) Copy() *KeeperInfo { return &nk } +// PostgresTimelinesHistory stores all PostgreSQL timelines belonging to this cluster type PostgresTimelinesHistory []*PostgresTimelineHistory -func (tlsh PostgresTimelinesHistory) Copy() PostgresTimelinesHistory { +// Copy returns a shallow copy +func (tlsh *PostgresTimelinesHistory) Copy() PostgresTimelinesHistory { if tlsh == nil { return nil } - ntlsh := make(PostgresTimelinesHistory, len(tlsh)) - copy(ntlsh, tlsh) + var ntlsh PostgresTimelinesHistory + for _, ptlh := range *tlsh { + ntlsh = append(ntlsh, &PostgresTimelineHistory{ + TimelineID: ptlh.TimelineID, + SwitchPoint: ptlh.SwitchPoint, + Reason: ptlh.Reason, + }) + } return ntlsh } +// PostgresTimelineHistory defines a PostgreSQL timeline type PostgresTimelineHistory struct { TimelineID uint64 SwitchPoint uint64 Reason string } +// GetTimelineHistory returns a PostgresTimelineHistory for a PostgresTimelinesHistory func (tlsh PostgresTimelinesHistory) GetTimelineHistory(id uint64) *PostgresTimelineHistory { for _, tlh := range tlsh { if tlh.TimelineID == id { @@ -61,6 +77,7 @@ func (tlsh PostgresTimelinesHistory) GetTimelineHistory(id uint64) *PostgresTime return nil } +// PostgresState defines the state of a PostgreSQL instance type PostgresState struct { Initialized bool Role common.Role @@ -70,6 +87,7 @@ type PostgresState struct { TimelinesHistory PostgresTimelinesHistory } +// Copy returns a shallow copy func (p *PostgresState) Copy() *PostgresState { if p == nil { return nil @@ -79,31 +97,32 @@ func (p *PostgresState) Copy() *PostgresState { return &np } -type KeepersDiscoveryInfo []*KeeperDiscoveryInfo - -type KeeperDiscoveryInfo struct { - ListenAddress string - Port string -} - +// SentinelsInfo stores all info on sentinels type SentinelsInfo []*SentinelInfo +/* func (s SentinelsInfo) Len() int { return len(s) } func (s SentinelsInfo) Less(i, j int) bool { return s[i].ID < s[j].ID } func (s SentinelsInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +*/ +// SentinelInfo stores all info on a sentinel type SentinelInfo struct { ID string ListenAddress string Port string } +// ProxiesInfo stores all info on proxies type ProxiesInfo []*ProxyInfo +/* func (p ProxiesInfo) Len() int { return len(p) } func (p ProxiesInfo) Less(i, j int) bool { return p[i].ID < p[j].ID } func (p ProxiesInfo) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +*/ +// ProxyInfo stores all info on a proxy type ProxyInfo struct { ID string ListenAddress string diff --git a/api/v0/member_test.go b/api/v0/member_test.go new file mode 100644 index 000000000..15d631e94 --- /dev/null +++ b/api/v0/member_test.go @@ -0,0 +1,113 @@ +package v0 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pgvillage-tools/stolon/internal/common" +) + +var _ = Describe("Member", func() { + When("Copying a KeeperInfo", func() { + var ( + orgKI, newKI *KeeperInfo + ) + BeforeEach(func() { + orgKI = randomKeeperInfo() + newKI = orgKI.Copy() + }) + It("should be a copy", func() { + newKI.ClusterViewVersion = randomInt() + Ω(newKI).NotTo(Equal(orgKI)) + }) + It("should copy all fields", func() { + Ω(newKI).To(Equal(orgKI)) + }) + It("should return nil when KeeperInfo is nil", func() { + var ki *KeeperInfo + Ω(ki.Copy()).To(BeNil()) + }) + It("should return empty struct when KeeperInfo is empty struct", func() { + var ki KeeperInfo + Ω(ki.Copy()).To(Equal(&KeeperInfo{})) + }) + }) + When("Copying a PostgresTimelinesHistory", func() { + var ( + orgPTH, newPTH PostgresTimelinesHistory + ) + BeforeEach(func() { + orgPTH = randomTLSH() + newPTH = orgPTH.Copy() + }) + It("should be a copy", func() { + newPTH[0].Reason = randomString() + Ω(newPTH).NotTo(Equal(orgPTH)) + }) + It("should copy all fields", func() { + Ω(newPTH).To(Equal(orgPTH)) + }) + It("should return nil when PostgresTimelinesHistory is nil", func() { + var pth *PostgresTimelinesHistory + Ω(pth.Copy()).To(BeNil()) + }) + It("should return nil when PostgresTimelinesHistory is nil list", func() { + var pth PostgresTimelinesHistory + Ω(pth.Copy()).To(BeNil()) + }) + }) + When("Copying a PostgresState", func() { + var ( + orgPS, newPS *PostgresState + ) + BeforeEach(func() { + orgPS = &PostgresState{ + Initialized: randomBool(), + Role: common.Role(randomString()), + SystemID: randomString(), + TimelineID: randomUInt64(), + XLogPos: randomUInt64(), + TimelinesHistory: randomTLSH(), + } + newPS = orgPS.Copy() + }) + It("should be a copy", func() { + newPS.TimelineID = randomUInt64() + Ω(newPS).NotTo(Equal(orgPS)) + }) + It("should copy all fields", func() { + Ω(newPS).To(Equal(orgPS)) + }) + It("should return nil when PostgresState is nil", func() { + var ps *PostgresState + Ω(ps.Copy()).To(BeNil()) + }) + It("should return nil when PostgresState is nil list", func() { + var ps PostgresState + Ω(ps.Copy()).To(Equal(&PostgresState{})) + }) + }) + When("Geting a timeline history", func() { + var ( + tlid = randomUInt64() + switchPoint = randomUInt64() + reason = randomString + orgPgTlH = PostgresTimelineHistory{ + TimelineID: tlid, + SwitchPoint: switchPoint, + Reason: reason(), + } + pgtlhs = PostgresTimelinesHistory{ + randomTLH(), + randomTLH(), + &orgPgTlH, + randomTLH(), + randomTLH(), + randomTLH(), + } + ) + It("should work as expected", func() { + fetchedTLH := pgtlhs.GetTimelineHistory(tlid) + Ω(*fetchedTLH).To(Equal(orgPgTlH)) + }) + }) +}) diff --git a/api/v0/random_utils_test.go b/api/v0/random_utils_test.go new file mode 100644 index 000000000..c56995eb5 --- /dev/null +++ b/api/v0/random_utils_test.go @@ -0,0 +1,96 @@ +package v0 + +import ( + "fmt" + "math/rand" +) + +func randomInt() int { return rand.Int() } +func randomUInt64() uint64 { return rand.Uint64() } +func randomString() string { return fmt.Sprintf("%x", rand.Int63()) } +func randomBool() bool { return (rand.Int()%2 == 1) } +func randomIP() string { + return fmt.Sprintf("%d.%d.%d.%d", rand.Uint32()%256, rand.Uint32()%256, + rand.Uint32()%256, rand.Uint32()%256) +} +func randomPort() string { return fmt.Sprintf("%d", rand.Uint32()) } + +func randomKeeperInfo() *KeeperInfo { + return &KeeperInfo{ + ID: randomString(), + ClusterViewVersion: rand.Int(), + ListenAddress: randomString(), + Port: randomPort(), + PGListenAddress: randomIP(), + PGPort: randomPort(), + } +} + +func randomKeeperState() *KeeperState { + return &KeeperState{ + ID: randomString(), + Healthy: randomBool(), + ClusterViewVersion: rand.Int(), + ListenAddress: randomIP(), + Port: randomPort(), + PGListenAddress: randomIP(), + PGPort: randomPort(), + PGState: &PostgresState{}, + } +} +func randomKeepersState(num uint) KeepersState { + kss := KeepersState{} + for i := uint(0); i < num; i++ { + ks := randomKeeperState() + kss[ks.ID] = ks + } + return kss +} + +func randomTLH() *PostgresTimelineHistory { + return &PostgresTimelineHistory{ + TimelineID: rand.Uint64(), + SwitchPoint: rand.Uint64(), + Reason: randomString(), + } +} +func randomTLSH() PostgresTimelinesHistory { + return PostgresTimelinesHistory{ + randomTLH(), + } +} + +func randomKeeperRole() *KeeperRole { + return &KeeperRole{ + ID: randomString(), + Follow: randomString(), + } +} + +func randomKeepersRole(num uint) KeepersRole { + ksr := KeepersRole{} + for i := uint(0); i < num; i++ { + kr := randomKeeperRole() + ksr[kr.ID] = kr + } + return ksr +} + +func randomProxyConf() *ProxyConf { + return &ProxyConf{ + Host: randomIP(), + Port: randomPort(), + } +} + +func randomClusterView() *ClusterView { + var c NilConfig + c.MergeDefaults() + return &ClusterView{ + Version: randomInt(), + Master: randomString(), + KeepersRole: randomKeepersRole(3), + ProxyConf: randomProxyConf(), + Config: &c, + } +} diff --git a/api/v0/v0_suite_test.go b/api/v0/v0_suite_test.go new file mode 100644 index 000000000..f7c5c9d26 --- /dev/null +++ b/api/v0/v0_suite_test.go @@ -0,0 +1,13 @@ +package v0 + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestV0(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "V0 Suite") +} diff --git a/api/v1/cluster.go b/api/v1/cluster.go new file mode 100644 index 000000000..16b5e38ea --- /dev/null +++ b/api/v1/cluster.go @@ -0,0 +1,551 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package v1 defines all resources for version v1 +package v1 + +// TODO: Split into multiple modules +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/util" +) + +// TODO: Rename XLOG to WAL everywhere + +const ( + // CurrentCDFormatVersion represents the current version of the Cluster Data Format. + // It will be raised whenever we change the API + CurrentCDFormatVersion uint64 = 1 + + // DefaultStoreTimeout sets the default for a store timeout + DefaultStoreTimeout = 5 * time.Second + + // DefaultDBNotIncreasingXLogPosTimes sets a default for the number of checks that it is ok for stolon + // not to detect XLog position increases on a standby. If WAL position is not increased more then this value, + // the standby is assumed not to be syncing properly. + DefaultDBNotIncreasingXLogPosTimes = 10 + + // DefaultSleepInterval is the default for sleeps during checks + DefaultSleepInterval = 5 * time.Second + + // DefaultRequestTimeout is the default for a request to time out + DefaultRequestTimeout = 10 * time.Second + + // DefaultConvergenceTimeout sets the timeout for convergence (of primaries and replica's) to be successful. + DefaultConvergenceTimeout = 30 * time.Second + + // DefaultInitTimeout sets the default timeout fo initializing a new cluster + DefaultInitTimeout = 5 * time.Minute + + // DefaultSyncTimeout sets the default timeout for waiting for a database recovery + // (including the replay of WAL files in case of Point-In-Time-Recovery) + DefaultSyncTimeout = 0 + + // DefaultDBWaitReadyTimeout sets the default for Ready status (being able to connect to PostgreSQL) + DefaultDBWaitReadyTimeout = 60 * time.Second + + // DefaultFailInterval sets the default for the cluster to be assumed unhealthy + DefaultFailInterval = 20 * time.Second + + // DefaultDeadKeeperRemovalInterval is the interval after which a keeper is assumed dead and is removed from the + // config. + DefaultDeadKeeperRemovalInterval = 48 * time.Hour + + // DefaultProxyCheckInterval is the default for the interval for the proxy to check the endpoint + DefaultProxyCheckInterval = 5 * time.Second + + // DefaultProxyTimeout is the default for the proxy check timeout. Once expired, all connections will be closed. + DefaultProxyTimeout = 15 * time.Second + + // DefaultMaxStandbys sets the default for teh number of standby's per keeper (input for max_replication_slots and + // max_wal_senders) + DefaultMaxStandbys uint16 = 20 + + // DefaultMaxStandbysPerSender sets the default for number of standby's before cleanup of old standby's is triggered + DefaultMaxStandbysPerSender uint16 = 3 + + // DefaultMaxStandbyLag sets the default for maximum standby lag for sync replica's (1MiB). + // Replicas with more lag are assumed not to be valid sync replica's (yet) + DefaultMaxStandbyLag = 1024 * 1204 + + // DefaultSynchronousReplication sets the default for sync replication when not set by config + DefaultSynchronousReplication = false + + // DefaultMinSynchronousStandbys sets the default for minimum number of sync standby's + DefaultMinSynchronousStandbys uint16 = 1 + + // DefaultMaxSynchronousStandbys sets the default for maximum number of sync standby's + DefaultMaxSynchronousStandbys uint16 = 1 + + // DefaultAdditionalWalSenders sets the default for additional wal-senders on top of as required for stolon managed + DefaultAdditionalWalSenders = 5 + + // DefaultUsePgrewind sets the default for using PgRewind (over starting over) + // TODO: enable pgrewind by default + DefaultUsePgrewind = false + + // DefaultMergePGParameter sets the default for merging parameters from local cluster to config after initialization + DefaultMergePGParameter = true + + // DefaultRole sets the default role for this cluster. A primary cluster has a primary, a Standby has only replica's + // and is replicating from another cluster + DefaultRole Role = Primary + + // DefaultSUReplAccess sets the default for replication access (strict: only from other standby's in this cluster to + // primary; all: any replication conenction is accepted when properly authenticated) + DefaultSUReplAccess SUReplAccessMode = SUReplAccessAll + + // DefaultAutomaticPgRestart sets the default for restarting postgres when required to apply config changes + DefaultAutomaticPgRestart = false +) + +const ( + // NoGeneration is the default Generation when not converging + NoGeneration int64 = 0 + // InitialGeneration is the default generation when starting convergence + InitialGeneration int64 = 1 +) + +// PGParameters is a map of all PostgreSQL config as configured for this cluster +type PGParameters map[string]string + +// PostgresBinaryVersion specifies the PostgreSQL binary version (major / minor) +type PostgresBinaryVersion struct { + Maj int + Min int +} + +// NewConfig defines the config for a new Cluster +type NewConfig struct { + Locale string `json:"locale,omitempty"` + Encoding string `json:"encoding,omitempty"` + DataChecksums bool `json:"dataChecksums,omitempty"` +} + +// PITRConfig defines the config for restoring using Point in time Recovery +type PITRConfig struct { + // DataRestoreCommand defines the command to execute for restoring the db + // cluster data). %d is replaced with the full path to the db cluster + // datadir. Use %% to embed an actual % character. + DataRestoreCommand string `json:"dataRestoreCommand,omitempty"` + ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` + RecoveryTargetSettings *RecoveryTargetSettings `json:"recoveryTargetSettings,omitempty"` +} + +// ExistingConfig defines the config for resuing an existing DB +type ExistingConfig struct { + KeeperUID string `json:"keeperUID,omitempty"` +} + +// StandbyConfig defines the config to be used for Replicas +type StandbyConfig struct { + StandbySettings *StandbySettings `json:"standbySettings,omitempty"` + ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` +} + +// ArchiveRecoverySettings defines the archive recovery settings in the recovery.conf file +// (https://www.postgresql.org/docs/9.6/static/archive-recovery-settings.html ) +type ArchiveRecoverySettings struct { + // value for restore_command + RestoreCommand string `json:"restoreCommand,omitempty"` +} + +// RecoveryTargetSettings defines the recovery target settings in the recovery.conf file +// (https://www.postgresql.org/docs/9.6/static/recovery-target-settings.html ) +type RecoveryTargetSettings struct { + RecoveryTarget string `json:"recoveryTarget,omitempty"` + RecoveryTargetLsn string `json:"recoveryTargetLsn,omitempty"` + RecoveryTargetName string `json:"recoveryTargetName,omitempty"` + RecoveryTargetTime string `json:"recoveryTargetTime,omitempty"` + RecoveryTargetXid string `json:"recoveryTargetXid,omitempty"` + RecoveryTargetTimeline string `json:"recoveryTargetTimeline,omitempty"` +} + +// StandbySettings defines the standby settings in the recovery.conf file +// (https://www.postgresql.org/docs/9.6/static/standby-settings.html ) +type StandbySettings struct { + PrimaryConninfo string `json:"primaryConninfo,omitempty"` + PrimarySlotName string `json:"primarySlotName,omitempty"` + RecoveryMinApplyDelay string `json:"recoveryMinApplyDelay,omitempty"` +} + +// SUReplAccessMode is an ENUM for Access Mode for Superusers (in HBA) +type SUReplAccessMode string + +const ( + // SUReplAccessAll allows access from every host + SUReplAccessAll SUReplAccessMode = "all" + // SUReplAccessStrict allows access from standby server IPs only + SUReplAccessStrict SUReplAccessMode = "strict" +) + +// Spec holds the cluster wide configuration +type Spec struct { + // Interval to wait before next check + SleepInterval *Duration `json:"sleepInterval,omitempty"` + // Time after which any request (keepers checks from sentinel etc...) will fail. + RequestTimeout *Duration `json:"requestTimeout,omitempty"` + // Interval to wait for a db to be converged to the required state when + // no long operation are expected. + ConvergenceTimeout *Duration `json:"convergenceTimeout,omitempty"` + // Interval to wait for a db to be initialized (doing a initdb) + InitTimeout *Duration `json:"initTimeout,omitempty"` + // Interval to wait for a db to be synced with a master + SyncTimeout *Duration `json:"syncTimeout,omitempty"` + // Interval to wait for a db to boot and become ready + DBWaitReadyTimeout *Duration `json:"dbWaitReadyTimeout,omitempty"` + // Interval after the first fail to declare a keeper or a db as not healthy. + FailInterval *Duration `json:"failInterval,omitempty"` + // Interval after which a dead keeper will be removed from the cluster data + DeadKeeperRemovalInterval *Duration `json:"deadKeeperRemovalInterval,omitempty"` + // Interval to wait before next proxy check + ProxyCheckInterval *Duration `json:"proxyCheckInterval,omitempty"` + // Interval where the proxy must successfully complete a check + ProxyTimeout *Duration `json:"proxyTimeout,omitempty"` + // Max number of standbys. This needs to be greater enough to cover both + // standby managed by stolon and additional standbys configured by the + // user. Its value affect different postgres parameters like + // max_replication_slots and max_wal_senders. Setting this to a number + // lower than the sum of stolon managed standbys and user managed + // standbys will have unpredicatable effects due to problems creating + // replication slots or replication problems due to exhausted wal + // senders. + MaxStandbys *uint16 `json:"maxStandbys,omitempty"` + // Max number of standbys for every sender. A sender can be a master or + // another standby (if/when implementing cascading replication). + MaxStandbysPerSender *uint16 `json:"maxStandbysPerSender,omitempty"` + // Max lag in bytes that an asynchronous standy can have to be elected in + // place of a failed master + MaxStandbyLag *uint32 `json:"maxStandbyLag,omitempty"` + // Use Synchronous replication between master and its standbys + SynchronousReplication *bool `json:"synchronousReplication,omitempty"` + // MinSynchronousStandbys is the mininum number if synchronous standbys + // to be configured when SynchronousReplication is true + MinSynchronousStandbys *uint16 `json:"minSynchronousStandbys,omitempty"` + // MaxSynchronousStandbys is the maximum number if synchronous standbys + // to be configured when SynchronousReplication is true + MaxSynchronousStandbys *uint16 `json:"maxSynchronousStandbys,omitempty"` + // AdditionalWalSenders defines the number of additional wal_senders in + // addition to the ones internally defined by stolon + AdditionalWalSenders *uint16 `json:"additionalWalSenders"` + // AdditionalMasterReplicationSlots defines additional replication slots to + // be created on the master postgres instance. Replication slots not defined + // here will be dropped from the master instance (i.e. manually created + // replication slots will be removed). + AdditionalMasterReplicationSlots []string `json:"additionalMasterReplicationSlots"` + // Whether to use pg_rewind + UsePgrewind *bool `json:"usePgrewind,omitempty"` + // InitMode defines the cluster initialization mode. Current modes are: new, existing, pitr + InitMode *InitMode `json:"initMode,omitempty"` + // Whether to merge pgParameters of the initialized db cluster, useful + // the retain initdb generated parameters when InitMode is new, retain + // current parameters when initMode is existing or pitr. + MergePgParameters *bool `json:"mergePgParameters,omitempty"` + // Role defines the cluster operating role (master or standby of an external database) + Role *Role `json:"role,omitempty"` + // Init configuration used when InitMode is "new" + NewConfig *NewConfig `json:"newConfig,omitempty"` + // Point in time recovery init configuration used when InitMode is "pitr" + PITRConfig *PITRConfig `json:"pitrConfig,omitempty"` + // Existing init configuration used when InitMode is "existing" + ExistingConfig *ExistingConfig `json:"existingConfig,omitempty"` + // Standby config when role is standby + StandbyConfig *StandbyConfig `json:"standbyConfig,omitempty"` + // Define the mode of the default hba rules needed for replication by standby keepers + // (the su and repl auth methods will be the one provided in the keeper command line options) + // Values can be "all" or "strict", "all" allow access from all ips, "strict" restrict + // master access to standby servers ips. + // Default is "all" + DefaultSUReplAccessMode *SUReplAccessMode `json:"defaultSUReplAccessMode,omitempty"` + // Map of postgres parameters + PGParameters PGParameters `json:"pgParameters,omitempty"` + // Additional pg_hba.conf entries + // we don't set omitempty since we want to distinguish between null or empty slice + PGHBA []string `json:"pgHBA"` + // Enable automatic pg restart when pg parameters that requires restart changes + AutomaticPgRestart *bool `json:"automaticPgRestart"` +} + +// ClusterStatus stores the status info for this cluster +type ClusterStatus struct { + CurrentGeneration int64 `json:"currentGeneration,omitempty"` + Phase Phase `json:"phase,omitempty"` + // Master DB UID + Master string `json:"master,omitempty"` +} + +// Cluster wraps cluster config, cluster status, UUID, etc together +type Cluster struct { + UID string `json:"uid,omitempty"` + Generation int64 `json:"generation,omitempty"` + ChangeTime time.Time `json:"changeTime,omitempty"` + + Spec *Spec `json:"spec,omitempty"` + + Status ClusterStatus `json:"status,omitempty"` +} + +// DeepCopy copies the entire structure and returns a complete clone +func (c *Cluster) DeepCopy() (dc *Cluster) { + return util.DeepCopy(c) +} + +// DeepCopy copies the entire structure and returns a complete clone +func (s *Spec) DeepCopy() (dc *Spec) { + return util.DeepCopy(s) +} + +// DefSpec returns a new Spec with unspecified values populated with +// their defaults +func (c *Cluster) DefSpec() *Spec { + s := c.Spec + if s == nil { + s = &Spec{} + } + return s.WithDefaults() +} + +// WithDefaults returns a new Spec with unspecified values populated with +// their defaults +func (s *Spec) WithDefaults() *Spec { + // Take a copy of the input Spec since we don't want to change the original + s = s.DeepCopy() + if s.SleepInterval == nil { + s.SleepInterval = &Duration{Duration: DefaultSleepInterval} + } + if s.RequestTimeout == nil { + s.RequestTimeout = &Duration{Duration: DefaultRequestTimeout} + } + if s.ConvergenceTimeout == nil { + s.ConvergenceTimeout = &Duration{Duration: DefaultConvergenceTimeout} + } + if s.InitTimeout == nil { + s.InitTimeout = &Duration{Duration: DefaultInitTimeout} + } + if s.SyncTimeout == nil { + s.SyncTimeout = &Duration{Duration: DefaultSyncTimeout} + } + if s.DBWaitReadyTimeout == nil { + s.DBWaitReadyTimeout = &Duration{Duration: DefaultDBWaitReadyTimeout} + } + if s.FailInterval == nil { + s.FailInterval = &Duration{Duration: DefaultFailInterval} + } + if s.DeadKeeperRemovalInterval == nil { + s.DeadKeeperRemovalInterval = &Duration{Duration: DefaultDeadKeeperRemovalInterval} + } + if s.ProxyCheckInterval == nil { + s.ProxyCheckInterval = &Duration{Duration: DefaultProxyCheckInterval} + } + if s.ProxyTimeout == nil { + s.ProxyTimeout = &Duration{Duration: DefaultProxyTimeout} + } + if s.MaxStandbys == nil { + s.MaxStandbys = util.ToPtr(DefaultMaxStandbys) + } + if s.MaxStandbysPerSender == nil { + s.MaxStandbysPerSender = util.ToPtr(DefaultMaxStandbysPerSender) + } + if s.MaxStandbyLag == nil { + s.MaxStandbyLag = util.ToPtr(uint32(DefaultMaxStandbyLag)) + } + if s.SynchronousReplication == nil { + s.SynchronousReplication = util.ToPtr(DefaultSynchronousReplication) + } + if s.UsePgrewind == nil { + s.UsePgrewind = util.ToPtr(DefaultUsePgrewind) + } + if s.MinSynchronousStandbys == nil { + s.MinSynchronousStandbys = util.ToPtr(DefaultMinSynchronousStandbys) + } + if s.MaxSynchronousStandbys == nil { + s.MaxSynchronousStandbys = util.ToPtr(DefaultMaxSynchronousStandbys) + } + if s.AdditionalWalSenders == nil { + s.AdditionalWalSenders = util.ToPtr(uint16(DefaultAdditionalWalSenders)) + } + if s.MergePgParameters == nil { + s.MergePgParameters = util.ToPtr(DefaultMergePGParameter) + } + if s.DefaultSUReplAccessMode == nil { + v := DefaultSUReplAccess + s.DefaultSUReplAccessMode = &v + } + if s.Role == nil { + v := DefaultRole + s.Role = &v + } + if s.AutomaticPgRestart == nil { + s.AutomaticPgRestart = util.ToPtr(DefaultAutomaticPgRestart) + } + return s +} + +// Validate validates a cluster spec. +func (s *Spec) Validate() error { + s = s.WithDefaults() + if s.SleepInterval.Duration < 0 { + return errors.New("sleepInterval must be positive") + } + if s.RequestTimeout.Duration < 0 { + return errors.New("requestTimeout must be positive") + } + if s.ConvergenceTimeout.Duration < 0 { + return errors.New("convergenceTimeout must be positive") + } + if s.InitTimeout.Duration < 0 { + return errors.New("initTimeout must be positive") + } + if s.SyncTimeout.Duration < 0 { + return errors.New("syncTimeout must be positive") + } + if s.DBWaitReadyTimeout.Duration < 0 { + return errors.New("dbWaitReadyTimeout must be positive") + } + if s.FailInterval.Duration < 0 { + return errors.New("failInterval must be positive") + } + if s.DeadKeeperRemovalInterval.Duration < 0 { + return errors.New("deadKeeperRemovalInterval must be positive") + } + if s.ProxyCheckInterval.Duration < 0 { + return errors.New("proxyCheckInterval must be positive") + } + if s.ProxyTimeout.Duration < 0 { + return errors.New("proxyTimeout must be positive") + } + if s.ProxyCheckInterval.Duration >= s.ProxyTimeout.Duration { + return errors.New("proxyCheckInterval should be less than proxyTimeout") + } + if *s.MaxStandbys < 1 { + return errors.New("maxStandbys must be at least 1") + } + if *s.MaxStandbysPerSender < 1 { + return errors.New("maxStandbysPerSender must be at least 1") + } + if *s.MaxSynchronousStandbys < 1 { + return errors.New("maxSynchronousStandbys must be at least 1") + } + if *s.MaxSynchronousStandbys < *s.MinSynchronousStandbys { + return errors.New("maxSynchronousStandbys must be greater or equal to minSynchronousStandbys") + } + if s.InitMode == nil { + return errors.New("initMode undefined") + } + for _, replicationSlot := range s.AdditionalMasterReplicationSlots { + if err := validateReplicationSlot(replicationSlot); err != nil { + return err + } + } + + // The unique validation we're doing on pgHBA entries is that they don't contain a newline character + for _, e := range s.PGHBA { + if strings.Contains(e, "\n") { + return errors.New("pgHBA entries cannot contain newline characters") + } + } + + switch *s.InitMode { + case New: + if *s.Role == Replica { + return errors.New("invalid cluster role standby when initMode is \"new\"") + } + case ExistingCluster: + if s.ExistingConfig == nil { + return errors.New("existingConfig undefined. Required when initMode is \"existing\"") + } + if s.ExistingConfig.KeeperUID == "" { + return errors.New("existingConfig.keeperUID undefined") + } + case PITR: + if s.PITRConfig == nil { + return errors.New("pitrConfig undefined. Required when initMode is \"pitr\"") + } + if s.PITRConfig.DataRestoreCommand == "" { + return errors.New("pitrConfig.DataRestoreCommand undefined") + } + if s.PITRConfig.RecoveryTargetSettings != nil && *s.Role == Replica { + return errors.New("cannot define pitrConfig.RecoveryTargetSettings when required cluster role is standby") + } + default: + return fmt.Errorf("unknown initMode: %q", *s.InitMode) + } + + switch *s.DefaultSUReplAccessMode { + case SUReplAccessAll: + case SUReplAccessStrict: + default: + return fmt.Errorf("unknown defaultSUReplAccessMode: %q", *s.DefaultSUReplAccessMode) + } + + switch *s.Role { + case Primary: + case Replica: + if s.StandbyConfig == nil { + return errors.New("standbyConfig undefined. Required when cluster role is \"standby\"") + } + default: + return fmt.Errorf("unknown role: %q", *s.InitMode) + } + return nil +} + +func validateReplicationSlot(replicationSlot string) error { + if !postgresql.IsValidReplSlotName(replicationSlot) { + return fmt.Errorf("wrong replication slot name: %q", replicationSlot) + } + if common.IsStolonName(replicationSlot) { + return fmt.Errorf("replication slot name is reserved: %q", replicationSlot) + } + return nil +} + +// UpdateSpec will update the spec of a cluster +func (c *Cluster) UpdateSpec(ns *Spec) error { + s := c.Spec + if err := ns.Validate(); err != nil { + return fmt.Errorf("invalid cluster spec: %v", err) + } + ds := s.WithDefaults() + dns := ns.WithDefaults() + if *ds.InitMode != *dns.InitMode { + return errors.New("cannot change cluster init mode") + } + if *ds.Role == Primary && *dns.Role == Replica { + return errors.New("cannot update a cluster from master role to standby role") + } + c.Spec = ns + return nil +} + +// NewCluster returns a freshly initialized cluster object +func NewCluster(uid string, cs *Spec) *Cluster { + c := &Cluster{ + UID: uid, + Generation: InitialGeneration, + ChangeTime: time.Now(), + Spec: cs, + Status: ClusterStatus{ + Phase: Initializing, + }, + } + return c +} diff --git a/api/v1/cluster_suite_test.go b/api/v1/cluster_suite_test.go new file mode 100644 index 000000000..2645a99d5 --- /dev/null +++ b/api/v1/cluster_suite_test.go @@ -0,0 +1,13 @@ +package v1 + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCluster(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cluster Suite") +} diff --git a/api/v1/cluster_test.go b/api/v1/cluster_test.go new file mode 100644 index 000000000..29c243271 --- /dev/null +++ b/api/v1/cluster_test.go @@ -0,0 +1,230 @@ +package v1 + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pgvillage-tools/stolon/internal/util" +) + +var _ = Describe("Cluster", func() { + When("Copying a Cluster", func() { + var ( + orgCluster, newCluster *Cluster + ) + BeforeEach(func() { + orgCluster = randomCluster() + newCluster = orgCluster.DeepCopy() + }) + It("should be a copy", func() { + newCluster.Generation = randomInt64() + Ω(newCluster).NotTo(Equal(orgCluster)) + }) + It("should copy all fields", func() { + Ω(newCluster).To(Equal(orgCluster)) + }) + It("should return nil when Cluster is nil", func() { + var c *Cluster + Ω(c.DeepCopy()).To(BeNil()) + }) + It("should return empty struct when Cluster is empty struct", func() { + var c Cluster + Ω(c.DeepCopy()).To(Equal(&Cluster{})) + }) + }) + When("Requesting a cluster with a Default Spec", func() { + It("should be a copy", func() { + c := randomCluster() + spec := c.DefSpec() + spec.MaxStandbyLag = randomUInt32() + Ω(c.Spec.MaxStandbyLag).NotTo(Equal(spec.MaxStandbyLag)) + }) + It("should set defaults for unspecified values", func() { + c := Cluster{} + spec := c.DefSpec() + Ω(spec.SleepInterval).To(Equal(&Duration{Duration: DefaultSleepInterval})) + Ω(spec.RequestTimeout).To(Equal(&Duration{Duration: DefaultRequestTimeout})) + Ω(spec.ConvergenceTimeout).To(Equal(&Duration{Duration: DefaultConvergenceTimeout})) + Ω(spec.InitTimeout).To(Equal(&Duration{Duration: DefaultInitTimeout})) + Ω(spec.SyncTimeout).To(Equal(&Duration{Duration: DefaultSyncTimeout})) + Ω(spec.DBWaitReadyTimeout).To(Equal(&Duration{Duration: DefaultDBWaitReadyTimeout})) + Ω(spec.FailInterval).To(Equal(&Duration{Duration: DefaultFailInterval})) + Ω(spec.DeadKeeperRemovalInterval).To(Equal(&Duration{Duration: DefaultDeadKeeperRemovalInterval})) + Ω(spec.ProxyCheckInterval).To(Equal(&Duration{Duration: DefaultProxyCheckInterval})) + Ω(spec.ProxyTimeout).To(Equal(&Duration{Duration: DefaultProxyTimeout})) + Ω(spec.MaxStandbys).To(Equal(util.ToPtr(DefaultMaxStandbys))) + Ω(spec.MaxStandbysPerSender).To(Equal(util.ToPtr(DefaultMaxStandbysPerSender))) + Ω(spec.MaxStandbyLag).To(Equal(util.ToPtr(uint32(DefaultMaxStandbyLag)))) + Ω(spec.SynchronousReplication).To(Equal(util.ToPtr(DefaultSynchronousReplication))) + Ω(spec.UsePgrewind).To(Equal(util.ToPtr(DefaultUsePgrewind))) + Ω(spec.MinSynchronousStandbys).To(Equal(util.ToPtr(DefaultMinSynchronousStandbys))) + Ω(spec.MaxSynchronousStandbys).To(Equal(util.ToPtr(DefaultMaxSynchronousStandbys))) + Ω(spec.AdditionalWalSenders).To(Equal(util.ToPtr(uint16(DefaultAdditionalWalSenders)))) + Ω(spec.MergePgParameters).To(Equal(util.ToPtr(DefaultMergePGParameter))) + Ω(spec.DefaultSUReplAccessMode).To(Equal(util.ToPtr(DefaultSUReplAccess))) + Ω(spec.Role).To(Equal(util.ToPtr(DefaultRole))) + Ω(spec.AutomaticPgRestart).To(Equal(util.ToPtr(DefaultAutomaticPgRestart))) + }) + It("should not set defaults for specified values", func() { + c := randomCluster() + spec := c.DefSpec() + Ω(spec.SleepInterval).To(Equal(c.Spec.SleepInterval)) + Ω(spec.RequestTimeout).To(Equal(c.Spec.RequestTimeout)) + Ω(spec.ConvergenceTimeout).To(Equal(c.Spec.ConvergenceTimeout)) + Ω(spec.InitTimeout).To(Equal(c.Spec.InitTimeout)) + Ω(spec.SyncTimeout).To(Equal(c.Spec.SyncTimeout)) + Ω(spec.DBWaitReadyTimeout).To(Equal(c.Spec.DBWaitReadyTimeout)) + Ω(spec.FailInterval).To(Equal(c.Spec.FailInterval)) + Ω(spec.DeadKeeperRemovalInterval).To(Equal(c.Spec.DeadKeeperRemovalInterval)) + Ω(spec.ProxyCheckInterval).To(Equal(c.Spec.ProxyCheckInterval)) + Ω(spec.ProxyTimeout).To(Equal(c.Spec.ProxyTimeout)) + Ω(spec.MaxStandbys).To(Equal(c.Spec.MaxStandbys)) + Ω(spec.MaxStandbysPerSender).To(Equal(c.Spec.MaxStandbysPerSender)) + Ω(spec.MaxStandbyLag).To(Equal(c.Spec.MaxStandbyLag)) + Ω(spec.SynchronousReplication).To(Equal(c.Spec.SynchronousReplication)) + Ω(spec.UsePgrewind).To(Equal(c.Spec.UsePgrewind)) + Ω(spec.MinSynchronousStandbys).To(Equal(c.Spec.MinSynchronousStandbys)) + Ω(spec.MaxSynchronousStandbys).To(Equal(c.Spec.MaxSynchronousStandbys)) + Ω(spec.AdditionalWalSenders).To(Equal(c.Spec.AdditionalWalSenders)) + Ω(spec.MergePgParameters).To(Equal(c.Spec.MergePgParameters)) + Ω(spec.DefaultSUReplAccessMode).To(Equal(c.Spec.DefaultSUReplAccessMode)) + Ω(spec.Role).To(Equal(c.Spec.Role)) + Ω(spec.AutomaticPgRestart).To(Equal(c.Spec.AutomaticPgRestart)) + }) + }) + When("Copying a Spec", func() { + var ( + orgSpec, newSpec *Spec + ) + BeforeEach(func() { + orgSpec = randomSpec() + newSpec = orgSpec.DeepCopy() + }) + It("should be a copy", func() { + newSpec.SleepInterval = randomDuration() + Ω(newSpec).NotTo(Equal(orgSpec)) + }) + It("should copy all fields", func() { + Ω(newSpec).To(Equal(orgSpec)) + }) + It("should return nil when Spec is nil", func() { + var s *Spec + Ω(s.DeepCopy()).To(BeNil()) + }) + It("should return empty struct when Spec is empty struct", func() { + var s Spec + Ω(s.DeepCopy()).To(Equal(&Spec{})) + }) + }) + When("Validating a Spec", func() { + It("should return no error when fields are unset", func() { + var spec Spec + Ω(spec.Validate()).To(HaveOccurred()) + }) + It("should return an error when something is not properly set", func() { + var ( + invalidDuration = Duration{time.Duration(-1)} + invalidUInt16 = uint16(0) + invalidInitMode = InitMode("invalid") + initModeNew = New + initModeExistingCluster = ExistingCluster + initModePITR = PITR + roleReplica = Replica + roleInvalid = Role("invalid") + suram = SUReplAccessMode("invalid") + ) + for _, spec := range []Spec{ + {SleepInterval: &invalidDuration}, + {RequestTimeout: &invalidDuration}, + {ConvergenceTimeout: &invalidDuration}, + {InitTimeout: &invalidDuration}, + {SyncTimeout: &invalidDuration}, + {DBWaitReadyTimeout: &invalidDuration}, + {FailInterval: &invalidDuration}, + {DeadKeeperRemovalInterval: &invalidDuration}, + {ProxyCheckInterval: &invalidDuration}, + {ProxyTimeout: &invalidDuration}, + {MaxStandbys: &invalidUInt16}, + {MaxStandbysPerSender: &invalidUInt16}, + {MaxSynchronousStandbys: &invalidUInt16}, + {MaxSynchronousStandbys: &invalidUInt16, + MinSynchronousStandbys: util.ToPtr(uint16(1))}, + {InitMode: nil}, + {AdditionalMasterReplicationSlots: []string{"slot-1"}}, + {AdditionalMasterReplicationSlots: []string{"stolon_slot"}}, + {PGHBA: []string{"local all all md5\n"}}, + {InitMode: &invalidInitMode}, + {InitMode: &initModeNew, Role: &roleReplica}, + {InitMode: &initModeExistingCluster, ExistingConfig: nil}, + {InitMode: &initModeExistingCluster, + ExistingConfig: &ExistingConfig{KeeperUID: ""}}, + {InitMode: &initModePITR, PITRConfig: nil}, + {InitMode: &initModePITR, + PITRConfig: &PITRConfig{DataRestoreCommand: ""}}, + {InitMode: &initModePITR, + PITRConfig: &PITRConfig{RecoveryTargetSettings: &RecoveryTargetSettings{}}, + Role: &roleReplica}, + {InitMode: &invalidInitMode}, + {DefaultSUReplAccessMode: &suram}, + {Role: &roleReplica, StandbyConfig: nil}, + {Role: &roleInvalid}, + } { + Ω(spec.Validate()).To(HaveOccurred()) + } + }) + }) + When("Updating a Spec", func() { + It("should succeed when all is good", func() { + var ( + initMode = New + specRole = Primary + spec = Spec{InitMode: &initMode, Role: &specRole} + cl = Cluster{Spec: &Spec{InitMode: &initMode, Role: &specRole}} + ) + Ω(cl.UpdateSpec(&spec)).Error().NotTo(HaveOccurred()) + }) + It("should return an error when the new spec is invalid", func() { + var ( + initMode = New + specRole = Primary + spec = Spec{InitMode: nil} + cl = Cluster{Spec: &Spec{InitMode: &initMode, Role: &specRole}} + ) + Ω(cl.UpdateSpec(&spec).Error()).To(Equal("invalid cluster spec: initMode undefined")) + }) + It("should return an error when initMode changes", func() { + var ( + specInitMode = PITR + spec = Spec{InitMode: &specInitMode, + PITRConfig: &PITRConfig{DataRestoreCommand: "/bin/true"}} + clInitMode = New + cl = Cluster{Spec: &Spec{InitMode: &clInitMode}} + ) + Ω(cl.UpdateSpec(&spec).Error()).To(Equal("cannot change cluster init mode")) + }) + It("should return an error when Role changes", func() { + var ( + initMode = ExistingCluster + specRole = Replica + existingConfig = ExistingConfig{KeeperUID: randomString()} + standbyConfig = StandbyConfig{StandbySettings: &StandbySettings{}, + ArchiveRecoverySettings: &ArchiveRecoverySettings{}} + spec = Spec{ + InitMode: &initMode, + Role: &specRole, + ExistingConfig: &existingConfig, + StandbyConfig: &standbyConfig, + } + clRole = Primary + cl = Cluster{Spec: &Spec{ + InitMode: &initMode, + Role: &clRole, + ExistingConfig: &existingConfig, + }} + ) + Ω(cl.UpdateSpec(&spec).Error()).To(Equal( + "cannot update a cluster from master role to standby role")) + }) + }) +}) diff --git a/api/v1/clusterdata.go b/api/v1/clusterdata.go new file mode 100644 index 000000000..705008a01 --- /dev/null +++ b/api/v1/clusterdata.go @@ -0,0 +1,72 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "reflect" + "time" + + "github.com/mitchellh/copystructure" +) + +// Data is meant to keep all the changes to the various components atomic (using +// an unique key) +type Data struct { + // ClusterData format version. Used to detect incompatible + // version and do upgrade. Needs to be bumped when a non + // backward compatible change is done to the other struct + // members. + FormatVersion uint64 `json:"formatVersion"` + ChangeTime time.Time `json:"changeTime"` + Cluster *Cluster `json:"cluster"` + Keepers Keepers `json:"keepers"` + DBs DBs `json:"dbs"` + Proxy *Proxy `json:"proxy"` +} + +// NewClusterData returns a freshly initialized Data object +func NewClusterData(c *Cluster) *Data { + return &Data{ + FormatVersion: CurrentCDFormatVersion, + Cluster: c, + Keepers: Keepers{}, + DBs: DBs{}, + Proxy: &Proxy{}, + } +} + +// DeepCopy copies the entire structure and returns a complete clone +func (d *Data) DeepCopy() (dc *Data) { + var ok bool + if nd, err := copystructure.Copy(d); err != nil { + panic(err) + } else if !reflect.DeepEqual(d, nd) { + panic("not equal") + } else if dc, ok = nd.(*Data); !ok { + panic("different type after copy") + } + return dc +} + +// FindDB can be used to find a DB belonging to a Keeper +func (d *Data) FindDB(keeper *Keeper) *DB { + for _, db := range d.DBs { + if db.Spec.KeeperUID == keeper.UID { + return db + } + } + return nil +} diff --git a/api/v1/clusterdata_test.go b/api/v1/clusterdata_test.go new file mode 100644 index 000000000..cbe8f309f --- /dev/null +++ b/api/v1/clusterdata_test.go @@ -0,0 +1,44 @@ +package v1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Clusterdata", func() { + When("Copying a Data", func() { + var ( + orgData, newData *Data + ) + BeforeEach(func() { + orgData = randomData() + newData = orgData.DeepCopy() + }) + It("should be a copy", func() { + newData.FormatVersion = randomUInt64() + Ω(newData).NotTo(Equal(orgData)) + }) + It("should copy all fields", func() { + Ω(newData).To(Equal(orgData)) + }) + It("should return nil when Data is nil", func() { + var d *Data + Ω(d.DeepCopy()).To(BeNil()) + }) + It("should return empty struct when Data is empty struct", func() { + var d Data + Ω(d.DeepCopy()).To(Equal(&Data{})) + }) + }) + When("Defining a new cluster data object", func() { + It("should return a well defined object", func() { + c := randomCluster() + d := NewClusterData(c) + Ω(d.FormatVersion).To(Equal(CurrentCDFormatVersion)) + Ω(d.Cluster).To(Equal(c)) + Ω(d.Keepers).NotTo(BeNil()) + Ω(d.DBs).NotTo(BeNil()) + Ω(d.Proxy).NotTo(BeNil()) + }) + }) +}) diff --git a/api/v1/db.go b/api/v1/db.go new file mode 100644 index 000000000..dc49a38d5 --- /dev/null +++ b/api/v1/db.go @@ -0,0 +1,111 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "time" + + "github.com/pgvillage-tools/stolon/internal/common" +) + +// DBSpec defines the spec of a database +type DBSpec struct { + // The KeeperUID this db is assigned to + KeeperUID string `json:"keeperUID,omitempty"` + // Time after which any request (keepers checks from sentinel etc...) will fail. + RequestTimeout Duration `json:"requestTimeout,omitempty"` + // See Spec MaxStandbys description + MaxStandbys uint16 `json:"maxStandbys,omitempty"` + // Use Synchronous replication between master and its standbys + SynchronousReplication bool `json:"synchronousReplication,omitempty"` + // Whether to use pg_rewind + UsePgrewind bool `json:"usePgrewind,omitempty"` + // AdditionalWalSenders defines the number of additional wal_senders in + // addition to the ones internally defined by stolon + AdditionalWalSenders uint16 `json:"additionalWalSenders"` + // AdditionalReplicationSlots is a list of additional replication slots. + // Replication slots not defined here will be dropped from the instance + // (i.e. manually created replication slots will be removed). + AdditionalReplicationSlots []string `json:"additionalReplicationSlots"` + // InitMode defines the db initialization mode. Current modes are: none, new + InitMode DBInitMode `json:"initMode,omitempty"` + // Init configuration used when InitMode is "new" + NewConfig *NewConfig `json:"newConfig,omitempty"` + // Point in time recovery init configuration used when InitMode is "pitr" + PITRConfig *PITRConfig `json:"pitrConfig,omitempty"` + // Map of postgres parameters + PGParameters PGParameters `json:"pgParameters,omitempty"` + // Additional pg_hba.conf entries + // We don't set omitempty since we want to distinguish between null or empty slice + PGHBA []string `json:"pgHBA"` + // DB Role (master or standby) + Role common.Role `json:"role,omitempty"` + // FollowConfig when Role is "standby" + FollowConfig *FollowConfig `json:"followConfig,omitempty"` + // Followers DB UIDs + Followers []string `json:"followers"` + // Whether to include previous postgresql.conf + IncludeConfig bool `json:"includePreviousConfig,omitempty"` + // SynchronousStandbys are the standbys to be configured as synchronous + SynchronousStandbys []string `json:"synchronousStandbys"` + // External SynchronousStandbys are external standbys names to be configured as synchronous + ExternalSynchronousStandbys []string `json:"externalSynchronousStandbys"` +} + +// DBStatus defines the status of a DB +type DBStatus struct { + Healthy bool `json:"healthy,omitempty"` + + CurrentGeneration int64 `json:"currentGeneration,omitempty"` + + ListenAddress string `json:"listenAddress,omitempty"` + Port string `json:"port,omitempty"` + + SystemID string `json:"systemdID,omitempty"` + TimelineID uint64 `json:"timelineID,omitempty"` + XLogPos uint64 `json:"xLogPos,omitempty"` + TimelinesHistory PostgresTimelinesHistory `json:"timelinesHistory,omitempty"` + + PGParameters PGParameters `json:"pgParameters,omitempty"` + + // DBUIDs of the internal standbys currently reported as in sync by the instance + CurSynchronousStandbys []string `json:"-"` + + // DBUIDs of the internal standbys that we know are in sync. + // They could be currently down but we know that they were reported as in + // sync in the past and they are defined inside synchronous_standby_names + // so the instance will wait for acknowledge from them. + SynchronousStandbys []string `json:"synchronousStandbys"` + + // NOTE(sgotti) we currently don't report the external synchronous standbys. + // If/when needed lets add a new ExternalSynchronousStandbys field + + OlderWalFile string `json:"olderWalFile,omitempty"` +} + +// DB is an instance in a cluster +type DB struct { + UID string `json:"uid,omitempty"` + Generation int64 `json:"generation,omitempty"` + ChangeTime time.Time `json:"changeTime,omitempty"` + + Spec *DBSpec `json:"spec,omitempty"` + + Status DBStatus `json:"status,omitempty"` +} + +// DBs can store all databases for a cluster +type DBs map[string]*DB diff --git a/api/v1/duration.go b/api/v1/duration.go new file mode 100644 index 000000000..ce3e5642e --- /dev/null +++ b/api/v1/duration.go @@ -0,0 +1,44 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "strings" + "time" +) + +// Duration is needed to be able to marshal/unmarshal json strings with time +// unit (eg. 3s, 100ms) instead of ugly times in nanoseconds. +type Duration struct { + time.Duration +} + +// MarshalJSON is needed for JSON serialization of Duration resources +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON is needed for JSON deserialization of Duration resources +func (d *Duration) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + du, err := time.ParseDuration(s) + if err != nil { + return err + } + d.Duration = du + return nil +} diff --git a/api/v1/duration_test.go b/api/v1/duration_test.go new file mode 100644 index 000000000..beea3edac --- /dev/null +++ b/api/v1/duration_test.go @@ -0,0 +1,42 @@ +package v1 + +import ( + "encoding/json" + "errors" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Duration", func() { + When("UnMarshalling", func() { + It("should work as expected", func() { + tests := []struct { + in string + d time.Duration + err error + }{ + {in: `"2ms"`, d: time.Millisecond * 2, err: nil}, + {in: `"3s"`, d: time.Second * 3, err: nil}, + {in: `"3h"`, d: time.Hour * 3, err: nil}, + {in: `2ms`, d: 0, err: errors.New("invalid character 'm' after top-level value")}, + {in: `"3 hours"`, d: 0, err: errors.New(`time: unknown unit " hours" in duration "3 hours"`)}, + {in: `"3"`, d: 0, err: errors.New(`time: missing unit in duration "3"`)}, + {in: `3`, d: 0, err: errors.New(`time: missing unit in duration "3"`)}, + } + + for _, tt := range tests { + dur := &Duration{} + err := json.Unmarshal([]byte(tt.in), &dur) + if tt.err != nil { + Ω(err).To(HaveOccurred()) + Ω(err.Error()).To(Equal(tt.err.Error())) + } else { + Ω(err).NotTo(HaveOccurred()) + Ω(dur.Duration).To(Equal(tt.d)) + } + } + }) + }) +}) diff --git a/api/v1/enums.go b/api/v1/enums.go new file mode 100644 index 000000000..cb379a043 --- /dev/null +++ b/api/v1/enums.go @@ -0,0 +1,85 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// FollowType is an enum for following internal (local primary keeper) or external (other cluster) +type FollowType string + +const ( + // FollowTypeInternal specifies that a db managed by a keeper in our cluster is followed + FollowTypeInternal FollowType = "internal" + // FollowTypeExternal specifies that a db in another cluster is followed + FollowTypeExternal FollowType = "external" +) + +// FollowConfig specifies the config for this keeper to follow another instance +type FollowConfig struct { + Type FollowType `json:"type,omitempty"` + // Keeper ID to follow when Type is "internal" + DBUID string `json:"dbuid,omitempty"` + // Standby settings when Type is "external" + StandbySettings *StandbySettings `json:"standbySettings,omitempty"` + ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` +} + +// Phase is an enum for the phase that a cluster is in +type Phase string + +const ( + // Initializing phase means the cluster is initializing + Initializing Phase = "initializing" + // Normal phase means the cluster is initialized and ready to be used + Normal Phase = "normal" +) + +// Role defines the role that a Keeper has +type Role string + +const ( + // Primary means that bthe instance is promoted and available for read/write + Primary Role = "master" + // Replica means that the instance is replicating changes for another upstream replica or Primary + Replica Role = "standby" +) + +// InitMode is an enum for the init mode that a Cluster is in +type InitMode string + +const ( + // New initializes a cluster starting from a freshly initialized database cluster. Valid only when cluster role is + // master. + New InitMode = "new" + // PITR initializes a cluster doing a point in time recovery on a keeper. + PITR InitMode = "pitr" + // ExistingCluster reuses an already Initialized cluster + ExistingCluster InitMode = "existing" +) + +// DBInitMode is an enum for the init mode that a database is in +type DBInitMode string + +const ( + // NoDB does not initialize a database + NoDB DBInitMode = "none" + // ExistingDB reuses the existing db + ExistingDB DBInitMode = "existing" + // NewDB initializes a new database + NewDB DBInitMode = "new" + // PITRDB initialized a database using Point in time Recovery + PITRDB DBInitMode = "pitr" + // ResyncDB syncs a database from another source + ResyncDB DBInitMode = "resync" +) diff --git a/api/v1/keeper.go b/api/v1/keeper.go new file mode 100644 index 000000000..1dee56876 --- /dev/null +++ b/api/v1/keeper.go @@ -0,0 +1,112 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "sort" + "time" + + "github.com/pgvillage-tools/stolon/internal/util" +) + +// KeeperSpec defines a spec for a Keeper resource +type KeeperSpec struct{} + +// KeeperStatus defines all staus fields on a Keeper +type KeeperStatus struct { + Healthy bool `json:"healthy,omitempty"` + LastHealthyTime time.Time `json:"lastHealthyTime,omitempty"` + + BootUUID string `json:"bootUUID,omitempty"` + + PostgresBinaryVersion PostgresBinaryVersion `json:"postgresBinaryVersion,omitempty"` + + ForceFail bool `json:"forceFail,omitempty"` + + CanBeMaster *bool `json:"canBeMaster,omitempty"` + CanBeSynchronousReplica *bool `json:"canBeSynchronousReplica,omitempty"` +} + +// Keeper combines the spec, status and other fields belonging to a keeper +type Keeper struct { + // Keeper ID + UID string `json:"uid,omitempty"` + Generation int64 `json:"generation,omitempty"` + ChangeTime time.Time `json:"changeTime,omitempty"` + + Spec *KeeperSpec `json:"spec,omitempty"` + + Status KeeperStatus `json:"status,omitempty"` +} + +// NewKeeperFromKeeperInfo returns a freshly initialized keeper created from a KeeperInfo object +func NewKeeperFromKeeperInfo(ki *KeeperInfo) *Keeper { + return &Keeper{ + UID: ki.UID, + Generation: InitialGeneration, + ChangeTime: time.Time{}, + Spec: &KeeperSpec{}, + Status: KeeperStatus{ + Healthy: true, + LastHealthyTime: time.Now(), + BootUUID: ki.BootUUID, + }, + } +} + +// Keepers can store all keepers for a cluster +type Keepers map[string]*Keeper + +// SortedKeys returns all keys of the Keepers as a sorted list +func (kss Keepers) SortedKeys() []string { + keys := []string{} + for k := range kss { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// KeepersInfo stores all KeeperInfo resources belonging to this cluster +type KeepersInfo map[string]*KeeperInfo + +// DeepCopy returns a copy of the KeepersInfo resource +func (k *KeepersInfo) DeepCopy() (dc *KeepersInfo) { + return util.DeepCopy(k) +} + +// KeeperInfo can store all info belonging to a Keeper +type KeeperInfo struct { + // An unique id for this info, used to know when this the keeper info + // has been updated + InfoUID string `json:"infoUID,omitempty"` + + UID string `json:"uid,omitempty"` + ClusterUID string `json:"clusterUID,omitempty"` + BootUUID string `json:"bootUUID,omitempty"` + + PostgresBinaryVersion PostgresBinaryVersion `json:"postgresBinaryVersion,omitempty"` + + PostgresState *PostgresState `json:"postgresState,omitempty"` + + CanBeMaster *bool `json:"canBeMaster,omitempty"` + CanBeSynchronousReplica *bool `json:"canBeSynchronousReplica,omitempty"` +} + +// DeepCopy returns a copy of the KeeperInfo resource +func (k *KeeperInfo) DeepCopy() (dc *KeeperInfo) { + return util.DeepCopy(k) +} diff --git a/api/v1/keeper_test.go b/api/v1/keeper_test.go new file mode 100644 index 000000000..8ff421cca --- /dev/null +++ b/api/v1/keeper_test.go @@ -0,0 +1,117 @@ +package v1 + +import ( + "slices" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Keeper", func() { + When("Copying a KeepersInfo", func() { + var ( + orgKIs, newKIs KeepersInfo + ) + BeforeEach(func() { + orgKIs = randomKeepersInfo(3) + dc := orgKIs.DeepCopy() + newKIs = *dc + }) + It("should be a copy", func() { + for key := range newKIs { + newKIs[key].BootUUID = randomString() + } + Ω(newKIs).NotTo(Equal(orgKIs)) + }) + It("should copy all fields", func() { + Ω(newKIs).To(Equal(orgKIs)) + }) + It("should return empty struct when KeepersInfo is nil", func() { + var kis *KeepersInfo + Ω(kis.DeepCopy()).To(BeNil()) + }) + }) + When("Copying a KeeperInfo", func() { + var ( + orgKS, newKS *KeeperInfo + ) + BeforeEach(func() { + orgKS = randomKeeperInfo() + newKS = orgKS.DeepCopy() + }) + It("should be a copy", func() { + newKS.BootUUID = randomString() + Ω(newKS).NotTo(Equal(orgKS)) + }) + It("should copy all fields", func() { + Ω(newKS).To(Equal(orgKS)) + }) + It("should return nil when KeeperInfo is nil", func() { + var ki *KeeperInfo + Ω(ki.DeepCopy()).To(BeNil()) + }) + It("should return empty struct when KeeperInfo is empty struct", func() { + var ki KeeperInfo + Ω(ki.DeepCopy()).To(Equal(&KeeperInfo{})) + }) + }) + When("Copying a ProxiesInfo", func() { + var ( + orgPIs, newPIs ProxiesInfo + ) + BeforeEach(func() { + orgPIs = randomProxiesInfo(5) + dc := orgPIs.DeepCopy() + newPIs = *dc + }) + It("should be a copy", func() { + for key := range newPIs { + newPIs[key].Generation = randomInt64() + } + Ω(newPIs).NotTo(Equal(orgPIs)) + }) + It("should copy all fields", func() { + Ω(newPIs).To(Equal(orgPIs)) + }) + It("should return nil when ProxiesInfo is nil", func() { + var pis *ProxiesInfo + Ω(pis.DeepCopy()).To(BeNil()) + }) + }) + // NewKeeperFromKeeperInfo + When("Creating a nw KeeperInfo", func() { + var ( + ki *KeeperInfo + ) + BeforeEach(func() { + ki = randomKeeperInfo() + }) + It("should be a copy", func() { + k := NewKeeperFromKeeperInfo(ki) + Ω(k.UID).To(Equal(ki.UID)) + Ω(k.Generation).To(Equal(InitialGeneration)) + Ω(k.ChangeTime.IsZero()).To(BeTrue()) + Ω(k.Spec).To(Equal(&KeeperSpec{})) + Ω(k.Status.Healthy).To(BeTrue()) + Ω(k.Status.LastHealthyTime.IsZero()).NotTo(BeTrue()) + Ω(k.Status.BootUUID).To(Equal(ki.BootUUID)) + }) + }) + // Keepers.SortedKeys + When("Requesting keys of a Keepers object", func() { + It("should return sorted keys list", func() { + k := randomKeepers(10) + var keys []string + for key := range k { + keys = append(keys, key) + } + Ω(slices.IsSorted(keys)).NotTo(BeTrue()) + sortedKeys := k.SortedKeys() + Ω(slices.IsSorted(sortedKeys)).To(BeTrue()) + Ω(sortedKeys).NotTo(Equal(keys)) + slices.Sort(keys) + Ω(slices.IsSorted(keys)).To(BeTrue()) + Ω(sortedKeys).To(Equal(keys)) + }) + }) +}) diff --git a/internal/cluster/member.go b/api/v1/member.go similarity index 64% rename from internal/cluster/member.go rename to api/v1/member.go index e6e2b793e..e28d9977b 100644 --- a/internal/cluster/member.go +++ b/api/v1/member.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,72 +13,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cluster +package v1 import ( - "reflect" "time" - "github.com/sorintlab/stolon/internal/common" - - "github.com/mitchellh/copystructure" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/util" ) -type KeepersInfo map[string]*KeeperInfo - -func (k KeepersInfo) DeepCopy() KeepersInfo { - if k == nil { - return nil - } - nk, err := copystructure.Copy(k) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(k, nk) { - panic("not equal") - } - return nk.(KeepersInfo) -} - -type KeeperInfo struct { - // An unique id for this info, used to know when this the keeper info - // has been updated - InfoUID string `json:"infoUID,omitempty"` - - UID string `json:"uid,omitempty"` - ClusterUID string `json:"clusterUID,omitempty"` - BootUUID string `json:"bootUUID,omitempty"` - - PostgresBinaryVersion PostgresBinaryVersion `json:"postgresBinaryVersion,omitempty"` - - PostgresState *PostgresState `json:"postgresState,omitempty"` - - CanBeMaster *bool `json:"canBeMaster,omitempty"` - CanBeSynchronousReplica *bool `json:"canBeSynchronousReplica,omitempty"` -} - -func (k *KeeperInfo) DeepCopy() *KeeperInfo { - if k == nil { - return nil - } - nk, err := copystructure.Copy(k) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(k, nk) { - panic("not equal") - } - return nk.(*KeeperInfo) -} - +// PostgresTimelinesHistory stores all PostgreSQL timelines belonging to this cluster type PostgresTimelinesHistory []*PostgresTimelineHistory +// PostgresTimelineHistory defines a PostgreSQL timeline type PostgresTimelineHistory struct { TimelineID uint64 `json:"timelineID,omitempty"` SwitchPoint uint64 `json:"switchPoint,omitempty"` Reason string `json:"reason,omitempty"` } +// GetTimelineHistory returns a PostgresTimelineHistory for a PostgresTimelinesHistory func (tlsh PostgresTimelinesHistory) GetTimelineHistory(id uint64) *PostgresTimelineHistory { for _, tlh := range tlsh { if tlh.TimelineID == id { @@ -87,6 +42,7 @@ func (tlsh PostgresTimelinesHistory) GetTimelineHistory(id uint64) *PostgresTime return nil } +// PostgresState defines the state of a PostgreSQL instance type PostgresState struct { UID string `json:"uid,omitempty"` Generation int64 `json:"generation,omitempty"` @@ -106,30 +62,24 @@ type PostgresState struct { OlderWalFile string `json:"olderWalFile,omitempty"` } -func (p *PostgresState) DeepCopy() *PostgresState { - if p == nil { - return nil - } - np, err := copystructure.Copy(p) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(p, np) { - panic("not equal") - } - return np.(*PostgresState) +// DeepCopy returns a copy of the PostgresState resource +func (p *PostgresState) DeepCopy() (dc *PostgresState) { + return util.DeepCopy(p) } +// SentinelsInfo stores all SentinelInfo resources for a cluster type SentinelsInfo []*SentinelInfo func (s SentinelsInfo) Len() int { return len(s) } func (s SentinelsInfo) Less(i, j int) bool { return s[i].UID < s[j].UID } func (s SentinelsInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +// SentinelInfo stores all info for a sentinel type SentinelInfo struct { UID string } +// ProxyInfo stores all info for a proxy type ProxyInfo struct { // An unique id for this info, used to know when the proxy info // has been updated @@ -145,22 +95,15 @@ type ProxyInfo struct { ProxyTimeout time.Duration } +// ProxiesInfo stores inbfo about all Proxies for this cluster type ProxiesInfo map[string]*ProxyInfo -func (p ProxiesInfo) DeepCopy() ProxiesInfo { - if p == nil { - return nil - } - np, err := copystructure.Copy(p) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(p, np) { - panic("not equal") - } - return np.(ProxiesInfo) +// DeepCopy returns a copy of the ProxiesInfo resource +func (p *ProxiesInfo) DeepCopy() (dc *ProxiesInfo) { + return util.DeepCopy(p) } +// ToSlice converts the ProxiesInfo map into a slice of ProxyInfo resources func (p ProxiesInfo) ToSlice() ProxiesInfoSlice { pis := ProxiesInfoSlice{} for _, pi := range p { @@ -169,6 +112,7 @@ func (p ProxiesInfo) ToSlice() ProxiesInfoSlice { return pis } +// ProxiesInfoSlice defines a slice of ProxyInfo resources type ProxiesInfoSlice []*ProxyInfo func (p ProxiesInfoSlice) Len() int { return len(p) } diff --git a/api/v1/member_test.go b/api/v1/member_test.go new file mode 100644 index 000000000..49f93bd59 --- /dev/null +++ b/api/v1/member_test.go @@ -0,0 +1,107 @@ +package v1 + +import ( + "slices" + "sort" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Member", func() { + When("Copying a PostgresState", func() { + var ( + orgPS, newPS *PostgresState + ) + BeforeEach(func() { + orgPS = randomPostgresState() + newPS = orgPS.DeepCopy() + }) + It("should be a copy", func() { + newPS.Generation = randomInt64() + Ω(newPS).NotTo(Equal(orgPS)) + }) + It("should copy all fields", func() { + Ω(newPS).To(Equal(orgPS)) + }) + It("should return nil when PostgresState is nil", func() { + var ps *PostgresState + Ω(ps.DeepCopy()).To(BeNil()) + }) + It("should return empty struct when PostgresState is empty struct", func() { + var ps PostgresState + Ω(ps.DeepCopy()).To(Equal(&PostgresState{})) + }) + }) + When("finding a timeline history", func() { + It("should be a copy", func() { + pths := randomTLSH(5) + for _, pth := range pths { + Ω(pths.GetTimelineHistory(pth.TimelineID)).To(Equal(pth)) + } + }) + It("should return nil when requesting an unknown timeline", func() { + Ω(randomTLSH(5).GetTimelineHistory(randomUInt64())).To(BeNil()) + }) + }) + When("Converting a ProxiesInfo to a slice", func() { + It("should work as expected", func() { + psi := randomProxiesInfo(5) + psiList := psi.ToSlice() + Ω(psiList).To(HaveLen(len(psi))) + for _, value := range psi { + Ω(psiList).To(ContainElement(value)) + } + }) + }) + When("sorting a SentinelsInfo", func() { + var ssi SentinelsInfo + BeforeEach(func() { + ssi = randomSentinelsInfo(5) + sort.Sort(ssi) + if len(ssi) > 1 { + ssi[0], ssi[1] = ssi[1], ssi[0] + } + }) + It("should not be sorted when generated at random", func() { + var keys []string + for _, si := range ssi { + keys = append(keys, si.UID) + } + Ω(slices.IsSorted(keys)).NotTo(BeTrue()) + }) + It("should be sorted after doing the sort operation", func() { + sort.Sort(ssi) + var keys []string + for _, si := range ssi { + keys = append(keys, si.UID) + } + Ω(slices.IsSorted(keys)).To(BeTrue()) + }) + }) + When("sorting a ProxiesInfoSlice", func() { + var psi ProxiesInfoSlice + BeforeEach(func() { + psi = randomProxiesInfo(5).ToSlice() + sort.Sort(psi) + if len(psi) > 1 { + psi[0], psi[1] = psi[1], psi[0] + } + }) + It("should not be sorted when generated at random", func() { + var keys []string + for _, si := range psi { + keys = append(keys, si.UID) + } + Ω(slices.IsSorted(keys)).NotTo(BeTrue()) + }) + It("should be sorted after doing the sort operation", func() { + sort.Sort(psi) + var keys []string + for _, si := range psi { + keys = append(keys, si.UID) + } + Ω(slices.IsSorted(keys)).To(BeTrue()) + }) + }) +}) diff --git a/api/v1/proxy.go b/api/v1/proxy.go new file mode 100644 index 000000000..ed3b311ac --- /dev/null +++ b/api/v1/proxy.go @@ -0,0 +1,39 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "time" + +// ProxySpec defines the config of a Proxy instance +type ProxySpec struct { + MasterDBUID string `json:"masterDbUid,omitempty"` + EnabledProxies []string `json:"enabledProxies,omitempty"` +} + +// ProxyStatus defines the status of a Proxy instance +type ProxyStatus struct { +} + +// Proxy combines the Spec, Status and other config of a Proxy into one resource +type Proxy struct { + UID string `json:"uid,omitempty"` + Generation int64 `json:"generation,omitempty"` + ChangeTime time.Time `json:"changeTime,omitempty"` + + Spec ProxySpec `json:"spec,omitempty"` + + Status ProxyStatus `json:"status,omitempty"` +} diff --git a/api/v1/random_utils_test.go b/api/v1/random_utils_test.go new file mode 100644 index 000000000..c4b40c0d9 --- /dev/null +++ b/api/v1/random_utils_test.go @@ -0,0 +1,227 @@ +package v1 + +import ( + "fmt" + "math/rand" + "time" + + "github.com/pgvillage-tools/stolon/internal/util" +) + +const ( + maxPgVersion = 20 +) + +var ( + testInitMode = InitMode("test") + testRole = Role("test") + testSUReplAccessMode = SUReplAccessMode("test") + testHBA = []string{ + "local all all deny", + "host all all 0.0.0.0/0 deny", + } + testSettings = PGParameters{"min_wal_keep": "1G"} +) + +// func randomInt() int { return rand.Int() } +func randomInt64() int64 { return rand.Int63() } +func randomUInt16() *uint16 { var i = uint16(rand.Uint32() % 65536); return &i } +func randomUInt32() *uint32 { var i = rand.Uint32(); return &i } +func randomUInt64() uint64 { return rand.Uint64() } +func randomString() string { return fmt.Sprintf("%x", rand.Int63()) } +func randomBool() bool { return (rand.Int()%2 == 1) } +func randomIP() string { + return fmt.Sprintf("%d.%d.%d.%d", rand.Uint32()%256, rand.Uint32()%256, + rand.Uint32()%256, rand.Uint32()%256) +} +func randomPort() string { return fmt.Sprintf("%d", rand.Uint32()%65536) } + +const maxNano uint64 = 1000 * 1000 * 1000 * 3600 + +func randomDuration() *Duration { return &Duration{Duration: randomTimeDuration()} } +func randomTimeDuration() time.Duration { return time.Duration(rand.Uint64() % maxNano) } + +func randomTLH() *PostgresTimelineHistory { + return &PostgresTimelineHistory{ + TimelineID: rand.Uint64(), + SwitchPoint: rand.Uint64(), + Reason: randomString(), + } +} +func randomTLSH(num uint) PostgresTimelinesHistory { + var pths PostgresTimelinesHistory + for i := uint(0); i < num; i++ { + pths = append(pths, randomTLH()) + } + return pths +} + +func randomCluster() *Cluster { + return &Cluster{ + UID: randomString(), + Generation: randomInt64(), + Spec: randomSpec(), + } +} + +func randomSpec() *Spec { + return &Spec{ + SleepInterval: randomDuration(), + RequestTimeout: randomDuration(), + ConvergenceTimeout: randomDuration(), + InitTimeout: randomDuration(), + SyncTimeout: randomDuration(), + DBWaitReadyTimeout: randomDuration(), + FailInterval: randomDuration(), + DeadKeeperRemovalInterval: randomDuration(), + ProxyCheckInterval: randomDuration(), + ProxyTimeout: randomDuration(), + MaxStandbys: randomUInt16(), + MaxStandbysPerSender: randomUInt16(), + MaxStandbyLag: randomUInt32(), + SynchronousReplication: util.ToPtr(randomBool()), + MinSynchronousStandbys: randomUInt16(), + MaxSynchronousStandbys: randomUInt16(), + AdditionalWalSenders: randomUInt16(), + AdditionalMasterReplicationSlots: []string{ + randomString(), + }, + UsePgrewind: util.ToPtr(randomBool()), + InitMode: &testInitMode, + MergePgParameters: util.ToPtr(randomBool()), + Role: &testRole, + NewConfig: &NewConfig{ + Locale: "en/us", + Encoding: "utf8", + DataChecksums: true, + }, + DefaultSUReplAccessMode: &testSUReplAccessMode, + PGParameters: testSettings, + PGHBA: testHBA, + AutomaticPgRestart: util.ToPtr(randomBool()), + } +} + +func randomData() *Data { + return &Data{ + FormatVersion: randomUInt64(), + ChangeTime: time.Now(), + Cluster: randomCluster(), + Keepers: randomKeepers(3), + DBs: nil, + Proxy: nil, + } +} + +func randomKeepers(num uint) Keepers { + ks := Keepers{} + for i := 0; i < int(num); i++ { + k := randomKeeper() + ks[k.UID] = k + } + return ks +} + +func randomKeeper() *Keeper { + return &Keeper{ + UID: randomString(), + Generation: randomInt64(), + ChangeTime: time.Now(), + Spec: randomKeeperSpec(), + Status: KeeperStatus{}, + } +} + +func randomKeeperSpec() *KeeperSpec { + return &KeeperSpec{} +} + +func randomKeeperInfo() *KeeperInfo { + return &KeeperInfo{ + InfoUID: randomString(), + + UID: randomString(), + ClusterUID: randomString(), + BootUUID: randomString(), + + PostgresBinaryVersion: randomPostgresBinaryVersion(), + + PostgresState: nil, + + CanBeMaster: util.ToPtr(randomBool()), + CanBeSynchronousReplica: util.ToPtr(randomBool()), + } +} + +func randomKeepersInfo(num uint) KeepersInfo { + kis := KeepersInfo{} + for i := 0; i < int(num); i++ { + ki := randomKeeperInfo() + kis[ki.UID] = ki + } + + return kis +} +func randomPostgresBinaryVersion() PostgresBinaryVersion { + return PostgresBinaryVersion{ + Maj: rand.Int() % maxPgVersion, + Min: rand.Int() % maxPgVersion, + } +} + +func randomPostgresState() *PostgresState { + return &PostgresState{ + UID: randomString(), + Generation: randomInt64(), + + ListenAddress: randomIP(), + Port: randomPort(), + + Healthy: randomBool(), + + SystemID: randomString(), + TimelineID: randomUInt64(), + XLogPos: randomUInt64(), + TimelinesHistory: randomTLSH(3), + + PGParameters: nil, + SynchronousStandbys: []string{ + randomString(), + randomString(), + }, + OlderWalFile: randomString(), + } +} + +func randomProxyInfo() *ProxyInfo { + return &ProxyInfo{ + InfoUID: randomString(), + + UID: randomString(), + Generation: randomInt64(), + ProxyTimeout: randomTimeDuration(), + } +} + +func randomProxiesInfo(num uint) ProxiesInfo { + pis := ProxiesInfo{} + for i := 0; i < int(num); i++ { + pi := randomProxyInfo() + pis[pi.UID] = pi + } + + return pis +} + +func randomSentinelInfo() *SentinelInfo { + return &SentinelInfo{ + UID: randomString(), + } +} +func randomSentinelsInfo(num uint) SentinelsInfo { + ssi := SentinelsInfo{} + for i := 0; i < int(num); i++ { + ssi = append(ssi, randomSentinelInfo()) + } + return ssi +} diff --git a/cmd/common.go b/cmd/common.go index 56943e905..dbdd1e3bf 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,26 +13,28 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd is a package which provides utilities that underlie the specific command package cmd import ( + "context" + "errors" "fmt" "os" "path/filepath" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" - "github.com/sorintlab/stolon/internal/util" - "github.com/mattn/go-isatty" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" + "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "k8s.io/client-go/kubernetes" - _ "k8s.io/client-go/plugin/pkg/client/auth" ) +// CommonConfig is a struct specifying if certain objects are strings or booleans for example type CommonConfig struct { IsStolonCtl bool @@ -41,7 +44,7 @@ type CommonConfig struct { StoreCertFile string StoreKeyFile string StoreCAFile string - StoreSkipTlsVerify bool + StoreSkipTLSVerify bool ClusterName string MetricsListenAddress string LogColor bool @@ -54,29 +57,109 @@ type CommonConfig struct { StoreTimeout time.Duration } +// AddCommonFlags is a function that gives flags to commands func AddCommonFlags(cmd *cobra.Command, cfg *CommonConfig) { - cmd.PersistentFlags().StringVar(&cfg.ClusterName, "cluster-name", "", "cluster name") - cmd.PersistentFlags().StringVar(&cfg.StoreBackend, "store-backend", "", "store backend type (etcdv2/etcd, etcdv3, consul or kubernetes)") - cmd.PersistentFlags().StringVar(&cfg.StoreEndpoints, "store-endpoints", "", "a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul)") - cmd.PersistentFlags().DurationVar(&cfg.StoreTimeout, "store-timeout", cluster.DefaultStoreTimeout, "store request timeout") - cmd.PersistentFlags().StringVar(&cfg.StorePrefix, "store-prefix", common.StorePrefix, "the store base prefix") - cmd.PersistentFlags().StringVar(&cfg.StoreCertFile, "store-cert-file", "", "certificate file for client identification to the store") - cmd.PersistentFlags().StringVar(&cfg.StoreKeyFile, "store-key", "", "private key file for client identification to the store") - cmd.PersistentFlags().BoolVar(&cfg.StoreSkipTlsVerify, "store-skip-tls-verify", false, "skip store certificate verification (insecure!!!)") - cmd.PersistentFlags().StringVar(&cfg.StoreCAFile, "store-ca-file", "", "verify certificates of HTTPS-enabled store servers using this CA bundle") - cmd.PersistentFlags().StringVar(&cfg.MetricsListenAddress, "metrics-listen-address", "", "metrics listen address i.e \"0.0.0.0:8080\" (disabled by default)") - cmd.PersistentFlags().StringVar(&cfg.KubeResourceKind, "kube-resource-kind", "", `the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported)`) + cmd.PersistentFlags().StringVar( + &cfg.ClusterName, + "cluster-name", + "", + "cluster name") + + cmd.PersistentFlags().StringVar( + &cfg.StoreBackend, + "store-backend", + "", + "store backend type (etcdv2/etcd, etcdv3, consul or kubernetes)") + + cmd.PersistentFlags().StringVar( + &cfg.StoreEndpoints, + "store-endpoints", + "", + // revive:disable-next-line + "a comma-delimited list of store endpoints (use https scheme for tls communication) (defaults: http://127.0.0.1:2379 for etcd, http://127.0.0.1:8500 for consul)") + + cmd.PersistentFlags().DurationVar( + &cfg.StoreTimeout, + "store-timeout", + cluster.DefaultStoreTimeout, + "store request timeout") + + cmd.PersistentFlags().StringVar( + &cfg.StorePrefix, + "store-prefix", + common.StorePrefix, + "the store base prefix") + + cmd.PersistentFlags().StringVar( + &cfg.StoreCertFile, + "store-cert-file", + "", + "certificate file for client identification to the store") + + cmd.PersistentFlags().StringVar( + &cfg.StoreKeyFile, + "store-key", + "", + "private key file for client identification to the store") + + cmd.PersistentFlags().BoolVar( + &cfg.StoreSkipTLSVerify, + "store-skip-tls-verify", + false, + "skip store certificate verification (insecure!!!)") + + cmd.PersistentFlags().StringVar( + &cfg.StoreCAFile, + "store-ca-file", + "", + "verify certificates of HTTPS-enabled store servers using this CA bundle") + + cmd.PersistentFlags().StringVar( + &cfg.MetricsListenAddress, + "metrics-listen-address", + "", + "metrics listen address i.e \"0.0.0.0:8080\" (disabled by default)") + + cmd.PersistentFlags().StringVar( + &cfg.KubeResourceKind, + "kube-resource-kind", + "", + // revive:disable-next-line + `the k8s resource kind to be used to store stolon clusterdata and do sentinel leader election (only "configmap" is currently supported)`) if !cfg.IsStolonCtl { - cmd.PersistentFlags().BoolVar(&cfg.LogColor, "log-color", false, "enable color in log output (default if attached to a terminal)") - cmd.PersistentFlags().StringVar(&cfg.LogLevel, "log-level", "info", "debug, info (default), warn or error") + cmd.PersistentFlags().BoolVar( + &cfg.LogColor, + "log-color", + false, + "enable color in log output (default if attached to a terminal)") + cmd.PersistentFlags().StringVar( + &cfg.LogLevel, + "log-level", + "info", + "debug, info (default), warn or error") } if cfg.IsStolonCtl { - cmd.PersistentFlags().StringVar(&cfg.LogLevel, "log-level", "info", "debug, info (default), warn or error") - cmd.PersistentFlags().StringVar(&cfg.KubeConfig, "kubeconfig", "", "path to kubeconfig file. Overrides $KUBECONFIG") - cmd.PersistentFlags().StringVar(&cfg.KubeContext, "kube-context", "", "name of the kubeconfig context to use") - cmd.PersistentFlags().StringVar(&cfg.KubeNamespace, "kube-namespace", "", "name of the kubernetes namespace to use") + cmd.PersistentFlags().StringVar( + &cfg.LogLevel, + "log-level", + "info", + "debug, info (default), warn or error") + cmd.PersistentFlags().StringVar( + &cfg.KubeConfig, + "kubeconfig", + "", + "path to kubeconfig file. Overrides $KUBECONFIG") + cmd.PersistentFlags().StringVar( + &cfg.KubeContext, + "kube-context", + "", + "name of the kubeconfig context to use") + cmd.PersistentFlags().StringVar( + &cfg.KubeNamespace, + "kube-namespace", "", + "name of the kubernetes namespace to use") } } @@ -98,12 +181,13 @@ func init() { prometheus.MustRegister(clusterIdentifier) } +// CheckCommonConfig is a function that checks if two configs match and if not returns an error func CheckCommonConfig(cfg *CommonConfig) error { if cfg.ClusterName == "" { - return fmt.Errorf("cluster name required") + return errors.New("cluster name required") } if cfg.StoreBackend == "" { - return fmt.Errorf("store backend type required") + return errors.New("store backend type required") } switch cfg.StoreBackend { @@ -115,7 +199,7 @@ func CheckCommonConfig(cfg *CommonConfig) error { case "etcdv3": case "kubernetes": if cfg.KubeResourceKind == "" { - return fmt.Errorf("unspecified kubernetes resource kind") + return errors.New("unspecified kubernetes resource kind") } if cfg.KubeResourceKind != "configmap" { return fmt.Errorf("wrong kubernetes resource kind: %q", cfg.KubeResourceKind) @@ -134,27 +218,31 @@ func SetMetrics(cfg *CommonConfig, component string) { clusterIdentifier.WithLabelValues(cfg.ClusterName, component).Set(1) } +// IsColorLoggerEnable is a function which checks if logs should be colorized or not func IsColorLoggerEnable(cmd *cobra.Command, cfg *CommonConfig) bool { if cmd.PersistentFlags().Changed("log-color") { return cfg.LogColor - } else { - return isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd()) } + return isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd()) } -func NewKVStore(cfg *CommonConfig) (store.KVStore, error) { - return store.NewKVStore(store.Config{ - Backend: store.Backend(cfg.StoreBackend), - Endpoints: cfg.StoreEndpoints, - Timeout: cfg.StoreTimeout, - CertFile: cfg.StoreCertFile, - KeyFile: cfg.StoreKeyFile, - CAFile: cfg.StoreCAFile, - SkipTLSVerify: cfg.StoreSkipTlsVerify, - }) +// NewKVStore returns a new KVStore function object +func NewKVStore(ctx context.Context, cfg *CommonConfig) (store.KVStore, error) { + return store.NewKVStore( + ctx, + store.Config{ + Backend: store.BackendType(cfg.StoreBackend), + Endpoints: cfg.StoreEndpoints, + Timeout: cfg.StoreTimeout, + CertFile: cfg.StoreCertFile, + KeyFile: cfg.StoreKeyFile, + CAFile: cfg.StoreCAFile, + SkipTLSVerify: cfg.StoreSkipTLSVerify, + }) } -func NewStore(cfg *CommonConfig) (store.Store, error) { +// NewStore is function that returns a new store object +func NewStore(ctx context.Context, cfg *CommonConfig) (store.Store, error) { var s store.Store switch cfg.StoreBackend { @@ -165,9 +253,9 @@ func NewStore(cfg *CommonConfig) (store.Store, error) { case "etcdv3": storePath := filepath.Join(cfg.StorePrefix, cfg.ClusterName) - kvstore, err := NewKVStore(cfg) + kvstore, err := NewKVStore(ctx, cfg) if err != nil { - return nil, fmt.Errorf("cannot create kv store: %v", err) + return nil, fmt.Errorf("NewStore: cannot create etcdv3 store: %v", err) } s = store.NewKVBackedStore(kvstore, storePath) case "kubernetes": @@ -177,14 +265,15 @@ func NewStore(cfg *CommonConfig) (store.Store, error) { } s, err = store.NewKubeStore(kubecli, podName, namespace, cfg.ClusterName) if err != nil { - return nil, fmt.Errorf("cannot create store: %v", err) + return nil, fmt.Errorf("NewStore: cannot create k8s store: %v", err) } } return s, nil } -func NewElection(cfg *CommonConfig, uid string) (store.Election, error) { +// NewElection is function that returns a new election object +func NewElection(ctx context.Context, cfg *CommonConfig, uid string) (store.Election, error) { var election store.Election switch cfg.StoreBackend { @@ -195,11 +284,16 @@ func NewElection(cfg *CommonConfig, uid string) (store.Election, error) { case "etcdv3": storePath := filepath.Join(cfg.StorePrefix, cfg.ClusterName) - kvstore, err := NewKVStore(cfg) + kvstore, err := NewKVStore(ctx, cfg) if err != nil { - return nil, fmt.Errorf("cannot create kv store: %v", err) + return nil, fmt.Errorf("NewElection: cannot create kv store: %v", err) } - election = store.NewKVBackedElection(kvstore, filepath.Join(storePath, common.SentinelLeaderKey), uid, cfg.StoreTimeout) + election = store.NewKVBackedElection( + kvstore, + filepath.Join(storePath, + common.SentinelLeaderKey), + uid, + cfg.StoreTimeout) case "kubernetes": kubecli, podName, namespace, err := getKubeValues(cfg) if err != nil { diff --git a/cmd/keeper/cmd/keeper.go b/cmd/keeper/cmd/keeper.go index dd9b2aafa..025fe6919 100644 --- a/cmd/keeper/cmd/keeper.go +++ b/cmd/keeper/cmd/keeper.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,14 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd holds all CLI code for the keeper package cmd import ( "context" "encoding/json" + "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "os" @@ -34,23 +36,54 @@ import ( "time" "github.com/mitchellh/copystructure" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/flagutil" - slog "github.com/sorintlab/stolon/internal/log" - pg "github.com/sorintlab/stolon/internal/postgresql" - "github.com/sorintlab/stolon/internal/store" - "github.com/sorintlab/stolon/internal/util" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/flagutil" + "github.com/pgvillage-tools/stolon/internal/logging" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" + "github.com/rs/zerolog" "github.com/davecgh/go-spew/spew" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" - "go.uber.org/zap" ) -var log = slog.S() +const ( + errorMsgPgInst = "failed to stop pg instance" + errorMsgDbState = "failed to save db local state" + followedStr = "followedDB" + ownerRWPermisions = 0600 + ownerRWExecPermisions = 0700 + ownerRWOtherRPermisions = 0644 + decimal = 10 + + authTrust = "trust" + authMd5 = "md5" + authCert = "cert" + authIdent = "ident" + authPeer = "peer" + + connParamUser = "user" + connParamPassword = "password" + connParamHost = "host" + connParamPort = "port" + connParamAppName = "application_name" + connParamDbName = "dbname" + connParamSslMode = "sslmode" + + connTypeHost = "host" + connTypeHostSsl = "hostssl" + connTypeHostNoSsl = "hostnossl" + connTypeHostGssEnc = "hostgssenc" + connTypeHostNoGssEnc = "hostnogssenc" + + defaultDatabase = "postgres" +) +// CmdKeeper exports the main keeper process var CmdKeeper = &cobra.Command{ Use: "stolon-keeper", Run: keeper, @@ -62,11 +95,13 @@ const ( minWalKeepSegments = 8 ) +// KeeperLocalState can be used to define the local state for a keep process type KeeperLocalState struct { UID string ClusterUID string } +// DBLocalState can be used to store the local state for DB settings type DBLocalState struct { UID string Generation int64 @@ -78,40 +113,47 @@ type DBLocalState struct { InitPGParameters common.Parameters } -func (s *DBLocalState) DeepCopy() *DBLocalState { +// DeepCopy is a function that copies the state of a local database +func (s *DBLocalState) DeepCopy() (dc *DBLocalState) { + var ok bool if s == nil { return nil } - ns, err := copystructure.Copy(s) - if err != nil { + if ns, err := copystructure.Copy(s); err != nil { panic(err) - } - // paranoid test - if !reflect.DeepEqual(s, ns) { + } else if !reflect.DeepEqual(s, ns) { panic("not equal") + } else if dc, ok = ns.(*DBLocalState); !ok { + panic("different type after copy") } - return ns.(*DBLocalState) + return dc } type config struct { cmd.CommonConfig - uid string - dataDir string - debug bool - pgListenAddress string - pgAdvertiseAddress string - pgPort string - pgAdvertisePort string - pgBinPath string - pgReplAuthMethod string - pgReplUsername string - pgReplPassword string - pgReplPasswordFile string - pgSUAuthMethod string - pgSUUsername string - pgSUPassword string - pgSUPasswordFile string + uid string + dataDir string + walDir string + debug bool + pgListenAddress string + pgAdvertiseAddress string + pgPort string + pgAdvertisePort string + pgBinPath string + pgReplConnType string + pgReplAuthMethod string + pgReplLocalAuthMethod string + pgReplSslMode string + pgReplUsername string + pgReplPassword string + pgReplPasswordFile string + pgSUConnType string + pgSUAuthMethod string + pgSULocalAuthMethod string + pgSUUsername string + pgSUPassword string + pgSUPasswordFile string canBeMaster bool canBeSynchronousReplica bool @@ -121,21 +163,28 @@ type config struct { var cfg config func init() { + ctx := context.Background() cmd.AddCommonFlags(CmdKeeper, &cfg.CommonConfig) - + // revive:disable CmdKeeper.PersistentFlags().StringVar(&cfg.uid, "id", "", "keeper uid (must be unique in the cluster and can contain only lower-case letters, numbers and the underscore character). If not provided a random uid will be generated.") CmdKeeper.PersistentFlags().StringVar(&cfg.uid, "uid", "", "keeper uid (must be unique in the cluster and can contain only lower-case letters, numbers and the underscore character). If not provided a random uid will be generated.") CmdKeeper.PersistentFlags().StringVar(&cfg.dataDir, "data-dir", "", "data directory") + CmdKeeper.PersistentFlags().StringVar(&cfg.walDir, "wal-dir", "", "wal directory") CmdKeeper.PersistentFlags().StringVar(&cfg.pgListenAddress, "pg-listen-address", "", "postgresql instance listening address, local address used for the postgres instance. For all network interface, you can set the value to '*'.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgAdvertiseAddress, "pg-advertise-address", "", "postgresql instance address from outside. Use it to expose ip different than local ip with a NAT networking config") CmdKeeper.PersistentFlags().StringVar(&cfg.pgPort, "pg-port", "5432", "postgresql instance listening port") CmdKeeper.PersistentFlags().StringVar(&cfg.pgAdvertisePort, "pg-advertise-port", "", "postgresql instance port from outside. Use it to expose port different than local port with a PAT networking config") CmdKeeper.PersistentFlags().StringVar(&cfg.pgBinPath, "pg-bin-path", "", "absolute path to postgresql binaries. If empty they will be searched in the current PATH") - CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplAuthMethod, "pg-repl-auth-method", "md5", "postgres replication user auth method. Default is md5.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplConnType, "pg-repl-connection-type", connTypeHost, "postgres replication user connection type. Default is host.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplAuthMethod, "pg-repl-auth-method", authMd5, "postgres replication user auth method. Default is authMd5.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplLocalAuthMethod, "pg-repl-local-auth-method", "", "postgres replication user auth method. Default is same as pg-repl-auth-method.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplSslMode, "pg-repl-ssl-mode", "prefer", "postgres replication user ssl-mode. Default is prefer.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplUsername, "pg-repl-username", "", "postgres replication user name. Required. It'll be created on db initialization. Must be the same for all keepers.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplPassword, "pg-repl-password", "", "postgres replication user password. Only one of --pg-repl-password or --pg-repl-passwordfile must be provided. Must be the same for all keepers.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgReplPasswordFile, "pg-repl-passwordfile", "", "postgres replication user password file. Only one of --pg-repl-password or --pg-repl-passwordfile must be provided. Must be the same for all keepers.") - CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUAuthMethod, "pg-su-auth-method", "md5", "postgres superuser auth method. Default is md5.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUConnType, "pg-su-connection-type", connTypeHost, "postgres superuser connection type. Default is host.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUAuthMethod, "pg-su-auth-method", authMd5, "postgres superuser auth method. Default is authMd5.") + CmdKeeper.PersistentFlags().StringVar(&cfg.pgSULocalAuthMethod, "pg-su-local-auth-method", "", "postgres superuser auth method. Default is same as pg-su-auth-method.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUUsername, "pg-su-username", "", "postgres superuser user name. Used for keeper managed instance access and pg_rewind based synchronization. It'll be created on db initialization. Defaults to the name of the effective user running stolon-keeper. Must be the same for all keepers.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUPassword, "pg-su-password", "", "postgres superuser password. Only one of --pg-su-password or --pg-su-passwordfile must be provided. Must be the same for all keepers.") CmdKeeper.PersistentFlags().StringVar(&cfg.pgSUPasswordFile, "pg-su-passwordfile", "", "postgres superuser password file. Only one of --pg-su-password or --pg-su-passwordfile must be provided. Must be the same for all keepers)") @@ -144,12 +193,14 @@ func init() { CmdKeeper.PersistentFlags().BoolVar(&cfg.canBeMaster, "can-be-master", true, "prevent keeper from being elected as master") CmdKeeper.PersistentFlags().BoolVar(&cfg.canBeSynchronousReplica, "can-be-synchronous-replica", true, "prevent keeper from being chosen as synchronous replica") CmdKeeper.PersistentFlags().BoolVar(&cfg.disableDataDirLocking, "disable-data-dir-locking", false, "disable locking on data dir. Warning! It'll cause data corruptions if two keepers are concurrently running with the same data dir.") + // revive:enable + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) if err := CmdKeeper.PersistentFlags().MarkDeprecated("id", "please use --uid"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("--id is deprecated, please use --uid") } if err := CmdKeeper.PersistentFlags().MarkDeprecated("debug", "use --log-level=debug instead"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("--debug is deprecated, please use --log-level=debug instead") } } @@ -180,20 +231,23 @@ var managedPGParameters = []string{ "recovery_target_action", } -func readPasswordFromFile(filepath string) (string, error) { - fi, err := os.Lstat(filepath) +func readPasswordFromFile(ctx context.Context, filePath string) (string, error) { + fi, err := os.Lstat(filePath) if err != nil { - return "", fmt.Errorf("unable to read password from file %s: %v", filepath, err) + return "", fmt.Errorf("unable to read password from file %s: %v", filePath, err) } + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) - if fi.Mode() > 0600 { - //TODO: enforce this by exiting with an error. Kubernetes makes this file too open today. - log.Warnw("password file permissions are too open. This file should only be readable to the user executing stolon! Continuing...", "file", filepath, "mode", fmt.Sprintf("%#o", fi.Mode())) + if fi.Mode() > ownerRWPermisions { + // TODO: enforce this by exiting with an error. Kubernetes makes this file too open today. + logger.Warn().Str("file", filePath).Str("mode", fmt.Sprintf("%#o", fi.Mode())). + Msgf("password file permissions are too open. " + + "This file should only be readable to the user executing stolon! Continuing...") } - pwBytes, err := ioutil.ReadFile(filepath) + pwBytes, err := os.ReadFile(filePath) if err != nil { - return "", fmt.Errorf("unable to read password from file %s: %v", filepath, err) + return "", fmt.Errorf("unable to read password from file %s: %v", filePath, err) } return string(pwBytes), nil } @@ -202,25 +256,23 @@ func readPasswordFromFile(filepath string) (string, error) { // if there's an user provided wal_level pg parameters and if its value is // "logical" then returns it, otherwise returns the default ("hot_standby" for // pg < 9.6 or "replica" for pg >= 9.6). -func (p *PostgresKeeper) walLevel(db *cluster.DB) string { +func (p *PostgresKeeper) walLevel(ctx context.Context, db *cluster.DB) string { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) var additionalValidWalLevels = []string{ "logical", // pg >= 10 } - maj, min, err := p.pgm.BinaryVersion() + version, err := p.pgm.BinaryVersion() if err != nil { - // in case we fail to parse the binary version then log it and just use "hot_standby" that works for all versions - log.Warnf("failed to get postgres binary version: %v", err) + // in case we fail to parse the binary version then log it and just use "hot_standby" + // that works for all versions + logger.Warn().AnErr("err", err).Msg("failed to get postgres binary version") return "hot_standby" } // set default wal_level walLevel := "hot_standby" - if maj == 9 { - if min >= 6 { - walLevel = "replica" - } - } else if maj >= 10 { + if version.GreaterThanEqual(pg.V96) { walLevel = "replica" } @@ -266,21 +318,22 @@ func (p *PostgresKeeper) walKeepSize(db *cluster.DB) string { return walKeepSize } -func (p *PostgresKeeper) mandatoryPGParameters(db *cluster.DB) common.Parameters { +func (p *PostgresKeeper) mandatoryPGParameters(ctx context.Context, db *cluster.DB) common.Parameters { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) params := common.Parameters{ "unix_socket_directories": common.PgUnixSocketDirectories, - "wal_level": p.walLevel(db), + "wal_level": p.walLevel(ctx, db), "hot_standby": "on", } - maj, _, err := p.pgm.BinaryVersion() + version, err := p.pgm.BinaryVersion() if err != nil { // in case we fail to parse the binary version don't return any wal_keep_segments or wal_keep_size - log.Warnf("failed to get postgres binary version: %v", err) + logger.Warn().AnErr("err", err).Msg("failed to get postgres binary version") return params } - if maj >= 13 { + if version.GreaterThanEqual(pg.V13) { params["wal_keep_size"] = p.walKeepSize(db) } else { params["wal_keep_segments"] = fmt.Sprintf("%d", p.walKeepSegments(db)) @@ -290,65 +343,65 @@ func (p *PostgresKeeper) mandatoryPGParameters(db *cluster.DB) common.Parameters } func (p *PostgresKeeper) getSUConnParams(db, followedDB *cluster.DB) pg.ConnParams { - cp := pg.ConnParams{ - "user": p.pgSUUsername, - "host": followedDB.Status.ListenAddress, - "port": followedDB.Status.Port, - "application_name": common.StolonName(db.UID), - "dbname": "postgres", - // prefer ssl if available (already the default for postgres libpq but not for golang lib pq) - "sslmode": "prefer", - } - if p.pgSUAuthMethod != "trust" { - cp.Set("password", p.pgSUPassword) + cp := pg.ConnParams{}. + WithUser(p.pgSUUsername). + WithHost(followedDB.Status.ListenAddress). + WithSPort(followedDB.Status.Port). + WithAppName(common.StolonName(db.UID)). + WithDbName(defaultDatabase). + // This is currently only used for pgRewind, which requires a SU (repluser might not be enough). + // Pgrewind is the only feature using SU over remote connection + // and with that the only type using SU with sslmode. + // Therefore we have skipped extra config option for sslmode for SU, + // and reuse config for sslmode for repl user instead. + WithSSLMode(p.pgReplSslMode) + + if p.pgSUAuthMethod == authMd5 { + cp = cp.WithPassword(p.pgSUPassword) } return cp } func (p *PostgresKeeper) getReplConnParams(db, followedDB *cluster.DB) pg.ConnParams { - cp := pg.ConnParams{ - "user": p.pgReplUsername, - "host": followedDB.Status.ListenAddress, - "port": followedDB.Status.Port, - "application_name": common.StolonName(db.UID), - // prefer ssl if available (already the default for postgres libpq but not for golang lib pq) - "sslmode": "prefer", - } - if p.pgReplAuthMethod != "trust" { - cp.Set("password", p.pgReplPassword) + cp := pg.ConnParams{}. + WithUser(p.pgReplUsername). + WithHost(followedDB.Status.ListenAddress). + WithSPort(followedDB.Status.Port). + WithAppName(common.StolonName(db.UID)). + WithSSLMode("prefer") + if p.pgReplAuthMethod == authMd5 { + cp = cp.WithPassword(p.pgReplPassword) } return cp } func (p *PostgresKeeper) getLocalConnParams() pg.ConnParams { - cp := pg.ConnParams{ - "user": p.pgSUUsername, - "host": common.PgUnixSocketDirectories, - "port": p.pgPort, - "dbname": "postgres", + cp := pg.ConnParams{}. + WithUser(p.pgSUUsername). + WithHost(common.PgUnixSocketDirectories). + WithSPort(p.pgPort). + WithDbName(defaultDatabase) // no sslmode defined since it's not needed and supported over unix sockets - } - if p.pgSUAuthMethod != "trust" { - cp.Set("password", p.pgSUPassword) + if p.pgSUAuthMethod == authMd5 { + cp = cp.WithPassword(p.pgSUPassword) } return cp } func (p *PostgresKeeper) getLocalReplConnParams() pg.ConnParams { - cp := pg.ConnParams{ - "user": p.pgReplUsername, - "password": p.pgReplPassword, - "host": common.PgUnixSocketDirectories, - "port": p.pgPort, + cp := pg.ConnParams{}. + WithUser(p.pgReplUsername). + WithHost(common.PgUnixSocketDirectories). + WithSPort(p.pgPort) // no sslmode defined since it's not needed and supported over unix sockets - } - if p.pgReplAuthMethod != "trust" { - cp.Set("password", p.pgReplPassword) + + if p.pgReplAuthMethod == authMd5 { + cp = cp.WithPassword(p.pgReplPassword) } return cp } -func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { +func (p *PostgresKeeper) createPGParameters(ctx context.Context, db *cluster.DB) common.Parameters { parameters := common.Parameters{} // Include init parameters if include config is required @@ -365,7 +418,7 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { } // Add/Replace mandatory PGParameters - for k, v := range p.mandatoryPGParameters(db) { + for k, v := range p.mandatoryPGParameters(ctx, db) { parameters[k] = v } @@ -377,10 +430,11 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { // fail. // TODO(sgotti) changing max_replication_slots requires an // instance restart. - parameters["max_replication_slots"] = strconv.FormatUint(uint64(db.Spec.MaxStandbys), 10) + parameters["max_replication_slots"] = strconv.FormatUint(uint64(db.Spec.MaxStandbys), decimal) // Add some more wal senders, since also the keeper will use them // TODO(sgotti) changing max_wal_senders requires an instance restart. - parameters["max_wal_senders"] = strconv.FormatUint(uint64((db.Spec.MaxStandbys*2)+2+db.Spec.AdditionalWalSenders), 10) + parameters["max_wal_senders"] = strconv.FormatUint(uint64((db.Spec.MaxStandbys*2)+2+db.Spec.AdditionalWalSenders), + decimal) // required by pg_rewind (if data checksum is enabled it's ignored) if db.Spec.UsePgrewind { @@ -388,7 +442,9 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { } // Setup synchronous replication - if db.Spec.SynchronousReplication && (len(db.Spec.SynchronousStandbys) > 0 || len(db.Spec.ExternalSynchronousStandbys) > 0) { + if db.Spec.SynchronousReplication && + (len(db.Spec.SynchronousStandbys) > 0 || + len(db.Spec.ExternalSynchronousStandbys) > 0) { synchronousStandbys := []string{} for _, synchronousStandby := range db.Spec.SynchronousStandbys { synchronousStandbys = append(synchronousStandbys, common.StolonName(synchronousStandby)) @@ -405,7 +461,11 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { // master. And choosing the non synchronous one will cause the loss of // the transactions contained in the wal records not transmitted. if len(synchronousStandbys) > 1 { - parameters["synchronous_standby_names"] = fmt.Sprintf("%d (%s)", len(synchronousStandbys), strings.Join(synchronousStandbys, ",")) + parameters["synchronous_standby_names"] = fmt.Sprintf( + "%d (%s)", + len(synchronousStandbys), + strings.Join(synchronousStandbys, + ",")) } else { parameters["synchronous_standby_names"] = strings.Join(synchronousStandbys, ",") } @@ -416,7 +476,11 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters { return parameters } -func (p *PostgresKeeper) createRecoveryOptions(recoveryMode pg.RecoveryMode, standbySettings *cluster.StandbySettings, archiveRecoverySettings *cluster.ArchiveRecoverySettings, recoveryTargetSettings *cluster.RecoveryTargetSettings) *pg.RecoveryOptions { +func (p *PostgresKeeper) createRecoveryOptions( + recoveryMode pg.RecoveryMode, + standbySettings *cluster.StandbySettings, + archiveRecoverySettings *cluster.ArchiveRecoverySettings, + recoveryTargetSettings *cluster.RecoveryTargetSettings) *pg.RecoveryOptions { parameters := common.Parameters{} if standbySettings != nil { @@ -465,23 +529,31 @@ func (p *PostgresKeeper) createRecoveryOptions(recoveryMode pg.RecoveryMode, sta } } +// PostgresKeeper is a struct containing information about the postgres server like +// directories, addresses and binpath etc. type PostgresKeeper struct { cfg *config bootUUID string - dataDir string - pgListenAddress string - pgAdvertiseAddress string - pgPort string - pgAdvertisePort string - pgBinPath string - pgReplAuthMethod string - pgReplUsername string - pgReplPassword string - pgSUAuthMethod string - pgSUUsername string - pgSUPassword string + dataDir string + walDir string + pgListenAddress string + pgAdvertiseAddress string + pgPort string + pgAdvertisePort string + pgBinPath string + pgReplConnType string + pgReplAuthMethod string + pgReplLocalAuthMethod string + pgReplSslMode string + pgReplUsername string + pgReplPassword string + pgSUConnType string + pgSUAuthMethod string + pgSULocalAuthMethod string + pgSUUsername string + pgSUPassword string sleepInterval time.Duration requestTimeout time.Duration @@ -504,8 +576,12 @@ type PostgresKeeper struct { canBeSynchronousReplica *bool } -func NewPostgresKeeper(cfg *config, end chan error) (*PostgresKeeper, error) { - e, err := cmd.NewStore(&cfg.CommonConfig) +// NewPostgresKeeper is function which makes a new postgreskeeper +func NewPostgresKeeper(ctx context.Context, cfg *config, end chan error) (*PostgresKeeper, error) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + ctx, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) + e, err := cmd.NewStore(ctx, &cfg.CommonConfig) if err != nil { return nil, fmt.Errorf("cannot create store: %v", err) } @@ -522,18 +598,24 @@ func NewPostgresKeeper(cfg *config, end chan error) (*PostgresKeeper, error) { bootUUID: common.UUID(), dataDir: dataDir, - - pgListenAddress: cfg.pgListenAddress, - pgAdvertiseAddress: cfg.pgAdvertiseAddress, - pgPort: cfg.pgPort, - pgAdvertisePort: cfg.pgAdvertisePort, - pgBinPath: cfg.pgBinPath, - pgReplAuthMethod: cfg.pgReplAuthMethod, - pgReplUsername: cfg.pgReplUsername, - pgReplPassword: cfg.pgReplPassword, - pgSUAuthMethod: cfg.pgSUAuthMethod, - pgSUUsername: cfg.pgSUUsername, - pgSUPassword: cfg.pgSUPassword, + walDir: cfg.walDir, + + pgListenAddress: cfg.pgListenAddress, + pgAdvertiseAddress: cfg.pgAdvertiseAddress, + pgPort: cfg.pgPort, + pgAdvertisePort: cfg.pgAdvertisePort, + pgBinPath: cfg.pgBinPath, + pgReplConnType: cfg.pgReplConnType, + pgReplAuthMethod: cfg.pgReplAuthMethod, + pgReplLocalAuthMethod: cfg.pgReplLocalAuthMethod, + pgReplSslMode: cfg.pgReplSslMode, + pgReplUsername: cfg.pgReplUsername, + pgReplPassword: cfg.pgReplPassword, + pgSUConnType: cfg.pgSUConnType, + pgSUAuthMethod: cfg.pgSUAuthMethod, + pgSULocalAuthMethod: cfg.pgSULocalAuthMethod, + pgSUUsername: cfg.pgSUUsername, + pgSUPassword: cfg.pgSUPassword, sleepInterval: cluster.DefaultSleepInterval, requestTimeout: cluster.DefaultRequestTimeout, @@ -553,20 +635,27 @@ func NewPostgresKeeper(cfg *config, end chan error) (*PostgresKeeper, error) { return nil, fmt.Errorf("failed to load keeper local state file: %v", err) } if p.keeperLocalState.UID != "" && p.cfg.uid != "" && p.keeperLocalState.UID != p.cfg.uid { - log.Fatalf("saved uid %q differs from configuration uid: %q", p.keeperLocalState.UID, cfg.uid) + logger.Fatal(). + Str("saved uid", p.keeperLocalState.UID). + Str("config uid", cfg.uid). + Msg("saved uid differs from configuration uid") } if p.keeperLocalState.UID == "" { p.keeperLocalState.UID = cfg.uid if cfg.uid == "" { p.keeperLocalState.UID = common.UID() - log.Infow("uid generated", "uid", p.keeperLocalState.UID) + logger.Info(). + Str("uid", p.keeperLocalState.UID). + Msg("uid generated") } if err = p.saveKeeperLocalState(); err != nil { - log.Fatalf("error: %v", err) + logger.Info(). + AnErr("err", err). + Msg("error while saving local state") } } - log.Infow("keeper uid", "uid", p.keeperLocalState.UID) + logger.Info().Str("keeper uid", p.keeperLocalState.UID).Msg("") err = p.loadDBLocalState() if err != nil && !os.IsNotExist(err) { @@ -582,10 +671,11 @@ func (p *PostgresKeeper) dbLocalStateCopy() *DBLocalState { } func (p *PostgresKeeper) usePgrewind(db *cluster.DB) bool { - return p.pgSUUsername != "" && p.pgSUPassword != "" && db.Spec.UsePgrewind + return p.pgSUUsername != "" && (p.pgSUPassword != "" || p.pgSUAuthMethod == authCert) && db.Spec.UsePgrewind } -func (p *PostgresKeeper) updateKeeperInfo() error { +func (p *PostgresKeeper) updateKeeperInfo(ctx context.Context) error { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) p.localStateMutex.Lock() keeperUID := p.keeperLocalState.UID clusterUID := p.keeperLocalState.ClusterUID @@ -595,10 +685,10 @@ func (p *PostgresKeeper) updateKeeperInfo() error { return nil } - maj, min, err := p.pgm.BinaryVersion() + version, err := p.pgm.BinaryVersion() if err != nil { // in case we fail to parse the binary version then log it and just report maj and min as 0 - log.Warnf("failed to get postgres binary version: %v", err) + logger.Warn().AnErr("err", err).Msg("failed to get postgres binary version") } keeperInfo := &cluster.KeeperInfo{ @@ -607,10 +697,10 @@ func (p *PostgresKeeper) updateKeeperInfo() error { ClusterUID: clusterUID, BootUUID: p.bootUUID, PostgresBinaryVersion: cluster.PostgresBinaryVersion{ - Maj: maj, - Min: min, + Maj: int(version.Major()), + Min: int(version.Minor()), }, - PostgresState: p.getLastPGState(), + PostgresState: p.getLastPGState(ctx), CanBeMaster: p.canBeMaster, CanBeSynchronousReplica: p.canBeSynchronousReplica, @@ -618,18 +708,16 @@ func (p *PostgresKeeper) updateKeeperInfo() error { // The time to live is just to automatically remove old entries, it's // not used to determine if the keeper info has been updated. - if err := p.e.SetKeeperInfo(context.TODO(), keeperUID, keeperInfo, p.sleepInterval); err != nil { - return err - } - return nil + return p.e.SetKeeperInfo(ctx, keeperUID, keeperInfo, p.sleepInterval) } -func (p *PostgresKeeper) updatePGState(pctx context.Context) { +func (p *PostgresKeeper) updatePGState(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) p.pgStateMutex.Lock() defer p.pgStateMutex.Unlock() - pgState, err := p.GetPGState(pctx) + pgState, err := p.GetPGState(ctx) if err != nil { - log.Errorw("failed to get pg state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to get pg state") } p.lastPGState = pgState } @@ -639,16 +727,22 @@ func (p *PostgresKeeper) updatePGState(pctx context.Context) { // // Since postgres 9.6 (https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html) // `synchronous_standby_names` can be in one of two formats: -// num_sync ( standby_name [, ...] ) -// standby_name [, ...] +// +// num_sync ( standby_name [, ...] ) +// standby_name [, ...] +// // two examples for this: -// 2 (node1,node2) -// node1,node2 +// +// 2 (node1,node2) +// node1,node2 +// // TODO(sgotti) since postgres 10 (https://www.postgresql.org/docs/10/static/runtime-config-replication.html) // `synchronous_standby_names` can be in one of three formats: -// [FIRST] num_sync ( standby_name [, ...] ) -// ANY num_sync ( standby_name [, ...] ) -// standby_name [, ...] +// +// [FIRST] num_sync ( standby_name [, ...] ) +// ANY num_sync ( standby_name [, ...] ) +// standby_name [, ...] +// // since we are writing ourself the synchronous_standby_names we don't handle this case. // If needed, to better handle all the cases with also a better validation of // standby names we could use something like the parser used by postgres @@ -666,7 +760,7 @@ func parseSynchronousStandbyNames(s string) ([]string, error) { rest := strings.Join(spacesSplit[1:], " ") inBrackets := strings.TrimSpace(rest) if !strings.HasPrefix(inBrackets, "(") || !strings.HasSuffix(inBrackets, ")") { - return nil, fmt.Errorf("synchronous standby string has number but lacks brackets") + return nil, errors.New("synchronous standby string has number but lacks brackets") } withoutBrackets := strings.TrimRight(strings.TrimLeft(inBrackets, "("), ")") entries = strings.Split(withoutBrackets, ",") @@ -681,8 +775,9 @@ func parseSynchronousStandbyNames(s string) ([]string, error) { return entries, nil } -func (p *PostgresKeeper) GetInSyncStandbys() ([]string, error) { - inSyncStandbysFullName, err := p.pgm.GetSyncStandbys() +// GetInSyncStandbys is a function that returns the InSyncStandbys +func (p *PostgresKeeper) GetInSyncStandbys(ctx context.Context) ([]string, error) { + inSyncStandbysFullName, err := p.pgm.GetSyncStandbys(ctx) if err != nil { return nil, fmt.Errorf("failed to retrieve current sync standbys status from instance: %v", err) } @@ -697,7 +792,9 @@ func (p *PostgresKeeper) GetInSyncStandbys() ([]string, error) { return inSyncStandbys, nil } -func (p *PostgresKeeper) GetPGState(pctx context.Context) (*cluster.PostgresState, error) { +// GetPGState is a function that returns the state of the postgresserver +func (p *PostgresKeeper) GetPGState(ctx context.Context) (*cluster.PostgresState, error) { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) p.getPGStateMutex.Lock() defer p.getPGStateMutex.Unlock() // Just get one pgstate at a time to avoid exausting available connections @@ -715,50 +812,54 @@ func (p *PostgresKeeper) GetPGState(pctx context.Context) (*cluster.PostgresStat return pgState, err } if initialized { - pgParameters, err := p.pgm.GetConfigFilePGParameters() + pgParameters, err := p.pgm.GetConfigFilePGParameters(ctx) if err != nil { - log.Errorw("cannot get configured pg parameters", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("cannot get configured pg parameters") return pgState, nil } - log.Debugw("got configured pg parameters", "pgParameters", pgParameters) + logger.Debug().Any("pgParameters", pgParameters).Msg("got configured pg parameters") filteredPGParameters := common.Parameters{} for k, v := range pgParameters { if !util.StringInSlice(managedPGParameters, k) { filteredPGParameters[k] = v } } - log.Debugw("filtered out managed pg parameters", "filteredPGParameters", filteredPGParameters) + logger.Debug(). + Any("filteredPGParameters", filteredPGParameters). + Msg("filtered out managed pg parameters") pgState.PGParameters = filteredPGParameters - inSyncStandbys, err := p.GetInSyncStandbys() + inSyncStandbys, err := p.GetInSyncStandbys(ctx) if err != nil { - log.Errorw("failed to retrieve current in sync standbys from instance", zap.Error(err)) + logger.Error(). + AnErr("err", err). + Msg("failed to retrieve current in sync standbys from instance") return pgState, nil } pgState.SynchronousStandbys = inSyncStandbys - sd, err := p.pgm.GetSystemData() + sd, err := p.pgm.GetSystemData(ctx) if err != nil { - log.Errorw("error getting pg state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error getting pg state") return pgState, nil } pgState.SystemID = sd.SystemID pgState.TimelineID = sd.TimelineID pgState.XLogPos = sd.XLogPos - ctlsh, err := getTimeLinesHistory(pgState, p.pgm, maxPostgresTimelinesHistory) + ctlsh, err := getTimeLinesHistory(ctx, pgState, p.pgm, maxPostgresTimelinesHistory) if err != nil { - log.Errorw("error getting timeline history", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error getting timeline history") return pgState, nil } pgState.TimelinesHistory = ctlsh ow, err := p.pgm.OlderWalFile() if err != nil { - log.Warnw("error getting older wal file", zap.Error(err)) + logger.Warn().AnErr("err", err).Msg("error getting older wal file") } else { - log.Debugw("older wal file", "filename", ow) + logger.Warn().Str("filename", ow).Msg("older wal file") pgState.OlderWalFile = ow } pgState.Healthy = true @@ -767,14 +868,21 @@ func (p *PostgresKeeper) GetPGState(pctx context.Context) (*cluster.PostgresStat return pgState, nil } -func getTimeLinesHistory(pgState *cluster.PostgresState, pgm pg.PGManager, maxPostgresTimelinesHistory int) (cluster.PostgresTimelinesHistory, error) { +func getTimeLinesHistory( + ctx context.Context, + pgState *cluster.PostgresState, + pgm pg.PGManager, + maxPostgresTimelinesHistory int) ( + cluster.PostgresTimelinesHistory, + error) { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) ctlsh := cluster.PostgresTimelinesHistory{} // if timeline <= 1 then no timeline history file exists. if pgState.TimelineID > 1 { var tlsh []*pg.TimelineHistory - tlsh, err := pgm.GetTimelinesHistory(pgState.TimelineID) + tlsh, err := pgm.GetTimelinesHistory(ctx, pgState.TimelineID) if err != nil { - log.Errorw("error getting timeline history", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error getting timeline history") return ctlsh, err } if len(tlsh) > maxPostgresTimelinesHistory { @@ -792,41 +900,57 @@ func getTimeLinesHistory(pgState *cluster.PostgresState, pgm pg.PGManager, maxPo return ctlsh, nil } -func (p *PostgresKeeper) getLastPGState() *cluster.PostgresState { +func (p *PostgresKeeper) getLastPGState(ctx context.Context) *cluster.PostgresState { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) p.pgStateMutex.Lock() pgState := p.lastPGState.DeepCopy() p.pgStateMutex.Unlock() - log.Debugf("pgstate dump: %s", spew.Sdump(pgState)) + logger.Debug().Str("pgstate dump", spew.Sdump(pgState)).Msg("") return pgState } +// Start is function that starts PostgresKeeper func (p *PostgresKeeper) Start(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) endSMCh := make(chan struct{}) endPgStatecheckerCh := make(chan struct{}) endUpdateKeeperInfo := make(chan struct{}) var err error - var cd *cluster.ClusterData - cd, _, err = p.e.GetClusterData(context.TODO()) + var cd *cluster.Data + cd, _, err = p.e.GetClusterData(ctx) if err != nil { - log.Errorw("error retrieving cluster data", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error retrieving cluster data") } else if cd != nil { if cd.FormatVersion != cluster.CurrentCDFormatVersion { - log.Errorw("unsupported clusterdata format version", "version", cd.FormatVersion) + logger.Error().Uint64("version", cd.FormatVersion).Msg("unsupported clusterdata format version") } else if cd.Cluster != nil { p.sleepInterval = cd.Cluster.DefSpec().SleepInterval.Duration p.requestTimeout = cd.Cluster.DefSpec().RequestTimeout.Duration } } - log.Debugf("cd dump: %s", spew.Sdump(cd)) + logger.Debug().Any("cd dump", cd).Msg("") // TODO(sgotti) reconfigure the various configurations options // (RequestTimeout) after a changed cluster config - pgm := pg.NewManager(p.pgBinPath, p.dataDir, p.getLocalConnParams(), p.getLocalReplConnParams(), p.pgSUAuthMethod, p.pgSUUsername, p.pgSUPassword, p.pgReplAuthMethod, p.pgReplUsername, p.pgReplPassword, p.requestTimeout) + pgm := pg.NewManager( + p.pgBinPath, + p.dataDir, + p.walDir, + p.getLocalConnParams(), + p.getLocalReplConnParams(), + p.pgSUAuthMethod, + p.pgSUUsername, + p.pgSUPassword, + p.pgReplAuthMethod, + p.pgReplUsername, + p.pgReplPassword, + p.requestTimeout) + p.pgm = pgm - _ = p.pgm.StopIfStarted(true) + _ = p.pgm.StopIfStarted(ctx, true) smTimerCh := time.NewTimer(0).C updatePGStateTimerCh := time.NewTimer(0).C @@ -838,9 +962,9 @@ func (p *PostgresKeeper) Start(ctx context.Context) { select { case <-ctx.Done(): - log.Debugw("stopping stolon keeper") - if err = p.pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + logger.Debug().Msg("stopping stolon keeper") + if err = p.pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) } p.end <- nil return @@ -867,8 +991,8 @@ func (p *PostgresKeeper) Start(ctx context.Context) { case <-updateKeeperInfoTimerCh: go func() { - if err := p.updateKeeperInfo(); err != nil { - log.Errorw("failed to update keeper info", zap.Error(err)) + if err := p.updateKeeperInfo(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to update keeper info") } endUpdateKeeperInfo <- struct{}{} }() @@ -879,10 +1003,13 @@ func (p *PostgresKeeper) Start(ctx context.Context) { } } -func (p *PostgresKeeper) resync(db, masterDB, followedDB *cluster.DB, tryPgrewind bool) error { +func (p *PostgresKeeper) resync(ctx context.Context, db, masterDB, followedDB *cluster.DB, tryPgrewind bool) error { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) pgm := p.pgm replConnParams := p.getReplConnParams(db, followedDB) - standbySettings := &cluster.StandbySettings{PrimaryConninfo: replConnParams.ConnString(), PrimarySlotName: common.StolonName(db.UID)} + standbySettings := &cluster.StandbySettings{ + PrimaryConninfo: replConnParams.ConnString(), + PrimarySlotName: common.StolonName(db.UID)} // TODO(sgotti) Actually we don't check if pg_rewind is installed or if // postgresql version is > 9.5 since someone can also use an externally @@ -896,49 +1023,65 @@ func (p *PostgresKeeper) resync(db, masterDB, followedDB *cluster.DB, tryPgrewin // rewind that it targets the current primary, rather than whatever database we // follow. connParams := p.getSUConnParams(db, masterDB) - log.Infow("syncing using pg_rewind", "masterDB", masterDB.UID, "keeper", followedDB.Spec.KeeperUID) - if err := pgm.SyncFromFollowedPGRewind(connParams, p.pgSUPassword); err != nil { - // log pg_rewind error and fallback to pg_basebackup - log.Errorw("error syncing with pg_rewind", zap.Error(err)) - } else { + err := pgm.SyncFromFollowedPGRewind(ctx, connParams, p.pgSUPassword) + logger.Info(). + Str("masterDB", masterDB.UID). + Str("keeper", followedDB.Spec.KeeperUID). + Msg("syncing using pg_rewind") + if err == nil { pgm.SetRecoveryOptions(p.createRecoveryOptions(pg.RecoveryModeStandby, standbySettings, nil, nil)) return nil } + // log pg_rewind error and fallback to pg_basebackup + logger.Error().AnErr("err", err).Msg("error syncing with pg_rewind") } - maj, min, err := p.pgm.BinaryVersion() + version, err := p.pgm.BinaryVersion() if err != nil { // in case we fail to parse the binary version then log it and just don't use replSlot - log.Warnf("failed to get postgres binary version: %v", err) + logger.Warn().AnErr("err", err).Msg("failed to get postgres binary version") } replSlot := "" - if (maj == 9 && min >= 6) || maj > 10 { + if version.GreaterThanEqual(pg.V96) { replSlot = common.StolonName(db.UID) } - if err := pgm.RemoveAll(); err != nil { + if err := pgm.RemoveAllIfInitialized(ctx); err != nil { return fmt.Errorf("failed to remove the postgres data dir: %v", err) } - if slog.IsDebug() { - log.Debugw("syncing from followed db", "followedDB", followedDB.UID, "keeper", followedDB.Spec.KeeperUID, "replConnParams", fmt.Sprintf("%v", replConnParams)) + if logger.GetLevel() == zerolog.DebugLevel { + logger.Debug(). + Str(followedStr, followedDB.UID). + Str("keeper", followedDB.Spec.KeeperUID). + Any("replConnParams", replConnParams). + Msg("syncing from followed db") } else { - log.Infow("syncing from followed db", "followedDB", followedDB.UID, "keeper", followedDB.Spec.KeeperUID) + logger.Info(). + Str(followedStr, followedDB.UID). + Str("keeper", followedDB.Spec.KeeperUID). + Msg("syncing from followed db") } - if err := pgm.SyncFromFollowed(replConnParams, replSlot); err != nil { + if err := pgm.SyncFromFollowed(ctx, replConnParams, replSlot); err != nil { return fmt.Errorf("sync error: %v", err) } - log.Infow("sync succeeded") + logger.Info().Msg("sync succeeded") pgm.SetRecoveryOptions(p.createRecoveryOptions(pg.RecoveryModeStandby, standbySettings, nil, nil)) return nil } -// TODO(sgotti) unify this with the sentinel one. They have the same logic but one uses *cluster.PostgresState while the other *cluster.DB -func (p *PostgresKeeper) isDifferentTimelineBranch(followedDB *cluster.DB, pgState *cluster.PostgresState) bool { +// TODO(sgotti) unify this with the sentinel one. They have the same logic but one uses *cluster.PostgresState +// while the other *cluster.DB +func (p *PostgresKeeper) isDifferentTimelineBranch( + ctx context.Context, followedDB *cluster.DB, pgState *cluster.PostgresState) bool { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) if followedDB.Status.TimelineID < pgState.TimelineID { - log.Infow("followed instance timeline < than our timeline", "followedTimeline", followedDB.Status.TimelineID, "timeline", pgState.TimelineID) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("timeline", pgState.TimelineID). + Msg("followed instance timeline < than our timeline") return true } @@ -957,7 +1100,12 @@ func (p *PostgresKeeper) isDifferentTimelineBranch(followedDB *cluster.DB, pgSta if ftlh.SwitchPoint == tlh.SwitchPoint { return false } - log.Infow("followed instance timeline forked at a different xlog pos than our timeline", "followedTimeline", followedDB.Status.TimelineID, "followedXlogpos", ftlh.SwitchPoint, "timeline", pgState.TimelineID, "xlogpos", tlh.SwitchPoint) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("followedXlogpos", ftlh.SwitchPoint). + Uint64("timeline", pgState.TimelineID). + Uint64("xlogpos", tlh.SwitchPoint). + Msg("followed instance timeline forked at a different xlog pos than our timeline") return true } @@ -965,14 +1113,25 @@ func (p *PostgresKeeper) isDifferentTimelineBranch(followedDB *cluster.DB, pgSta ftlh := followedDB.Status.TimelinesHistory.GetTimelineHistory(pgState.TimelineID) if ftlh != nil { if ftlh.SwitchPoint < pgState.XLogPos { - log.Infow("followed instance timeline forked before our current state", "followedTimeline", followedDB.Status.TimelineID, "followedXlogpos", ftlh.SwitchPoint, "timeline", pgState.TimelineID, "xlogpos", pgState.XLogPos) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("followedXlogpos", ftlh.SwitchPoint). + Uint64("timeline", pgState.TimelineID). + Uint64("xlogpos", pgState.XLogPos). + Msg("followed instance timeline forked before our current state") return true } } return false } -func (p *PostgresKeeper) updateReplSlots(curReplSlots []string, uid string, followersUIDs, additionalReplSlots []string) error { +func (p *PostgresKeeper) updateReplSlots( + ctx context.Context, + curReplSlots []string, + uid string, + followersUIDs, + additionalReplSlots []string) error { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) internalReplSlots := map[string]struct{}{} // Create a list of the wanted internal replication slots @@ -994,9 +1153,9 @@ func (p *PostgresKeeper) updateReplSlots(curReplSlots []string, uid string, foll continue } if _, ok := internalReplSlots[slot]; !ok { - log.Infow("dropping replication slot", "slot", slot) - if err := p.pgm.DropReplicationSlot(slot); err != nil { - log.Errorw("failed to drop replication slot", "slot", slot, "err", err) + logger.Info().Str("slot", slot).Msg("dropping replication slot") + if err := p.pgm.DropReplicationSlot(ctx, slot); err != nil { + logger.Error().AnErr("err", err).Str("slot", slot).Msg("failed to drop replication slot") // don't return the error but continue also if drop failed (standby still connected) } } @@ -1005,9 +1164,9 @@ func (p *PostgresKeeper) updateReplSlots(curReplSlots []string, uid string, foll // Create internal replication slots for slot := range internalReplSlots { if !util.StringInSlice(curReplSlots, slot) { - log.Infow("creating replication slot", "slot", slot) - if err := p.pgm.CreateReplicationSlot(slot); err != nil { - log.Errorw("failed to create replication slot", "slot", slot, zap.Error(err)) + logger.Info().Str("slot", slot).Msg("creating replication slot") + if err := p.pgm.CreateReplicationSlot(ctx, slot); err != nil { + logger.Error().AnErr("err", err).Str("slot", slot).Msg("failed to create replication slot") return err } } @@ -1015,45 +1174,52 @@ func (p *PostgresKeeper) updateReplSlots(curReplSlots []string, uid string, foll return nil } -func (p *PostgresKeeper) refreshReplicationSlots(cd *cluster.ClusterData, db *cluster.DB) error { +func (p *PostgresKeeper) refreshReplicationSlots(ctx context.Context, _ *cluster.Data, db *cluster.DB) error { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) var currentReplicationSlots []string - currentReplicationSlots, err := p.pgm.GetReplicationSlots() + currentReplicationSlots, err := p.pgm.GetReplicationSlots(ctx) if err != nil { - log.Errorw("failed to get replication slots", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to get replication slots") return err } followersUIDs := db.Spec.Followers - if err = p.updateReplSlots(currentReplicationSlots, db.UID, followersUIDs, db.Spec.AdditionalReplicationSlots); err != nil { - log.Errorw("error updating replication slots", zap.Error(err)) + if err = p.updateReplSlots( + ctx, + currentReplicationSlots, + db.UID, + followersUIDs, + db.Spec.AdditionalReplicationSlots); err != nil { + logger.Error().AnErr("err", err).Msg("error updating replication slots") return err } return nil } -func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { +func (p *PostgresKeeper) postgresKeeperSM(ctx context.Context) { + ctx, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) e := p.e pgm := p.pgm - cd, _, err := e.GetClusterData(pctx) + cd, _, err := e.GetClusterData(ctx) if err != nil { - log.Errorw("error retrieving cluster data", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error retrieving cluster data") return } - log.Debugf("cd dump: %s", spew.Sdump(cd)) + logger.Debug().Any("clusterdata", cd).Msg("cd dump: %s") if cd == nil { - log.Infow("no cluster data available, waiting for it to appear") + logger.Info().Msg("no cluster data available, waiting for it to appear") return } if cd.FormatVersion != cluster.CurrentCDFormatVersion { - log.Errorw("unsupported clusterdata format version", "version", cd.FormatVersion) + logger.Error().Uint64("version", cd.FormatVersion).Msg("unsupported clusterdata format version") return } if err = cd.Cluster.Spec.Validate(); err != nil { - log.Errorw("clusterdata validation failed", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("clusterdata validation failed") return } @@ -1069,7 +1235,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { if p.keeperLocalState.ClusterUID != cd.Cluster.UID { p.keeperLocalState.ClusterUID = cd.Cluster.UID if err = p.saveKeeperLocalState(); err != nil { - log.Errorw("failed to save keeper local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to save keeper local state") return } } @@ -1077,29 +1243,32 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { k, ok := cd.Keepers[p.keeperLocalState.UID] if !ok { - log.Infow("our keeper data is not available, waiting for it to appear") + logger.Info().Msg("our keeper data is not available, waiting for it to appear") return } db := cd.FindDB(k) if db == nil { - log.Infow("no db assigned") - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + logger.Info().Msg("no db assigned") + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) } return } if p.bootUUID != k.Status.BootUUID { - log.Infow("our db boot UID is different than the cluster data one, waiting for it to be updated", "bootUUID", p.bootUUID, "clusterBootUUID", k.Status.BootUUID) - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + logger.Info(). + Str("bootUUID", p.bootUUID). + Str("clusterBootUUID", k.Status.BootUUID). + Msg("our db boot UID is different than the cluster data one, waiting for it to be updated") + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) } return } // Generate hba auth from clusterData - pgm.SetHba(p.generateHBA(cd, db, p.waitSyncStandbysSynced)) + pgm.SetHba(p.generateHBA(ctx, cd, db, p.waitSyncStandbysSynced)) var pgParameters common.Parameters @@ -1107,16 +1276,16 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { if dbls.Initializing { // If we are here this means that the db initialization or // resync has failed so we have to clean up stale data - log.Errorw("db failed to initialize or resync") + logger.Error().Msg("db failed to initialize or resync") - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) return } // Clean up cluster db datadir - if err = pgm.RemoveAll(); err != nil { - log.Errorw("failed to remove the postgres data dir", zap.Error(err)) + if err = pgm.RemoveAllIfInitialized(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to remove the postgres data dir") return } // Reset current db local state since it's not valid anymore @@ -1126,7 +1295,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { Initializing: false, } if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } } @@ -1135,17 +1304,20 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { var initialized bool initialized, err = pgm.IsInitialized() if err != nil { - log.Errorw("failed to detect if instance is initialized", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to detect if instance is initialized") return } - log.Infow("current db UID different than cluster data db UID", "db", p.dbLocalState.UID, "cdDB", db.UID) + logger.Info(). + Str("db", p.dbLocalState.UID). + Str("cdDB", db.UID). + Msg("current db UID different than cluster data db UID") pgm.SetRecoveryOptions(nil) p.waitSyncStandbysSynced = false switch db.Spec.InitMode { - case cluster.DBInitModeNew: - log.Infow("initializing the database cluster") + case cluster.NewDB: + logger.Info().Msg("initializing the database cluster") ndbls := &DBLocalState{ UID: db.UID, // Set a no generation since we aren't already converged. @@ -1153,12 +1325,12 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { Initializing: true, } if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } // create postgres parameters with empty InitPGParameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // update pgm postgres parameters pgm.SetParameters(pgParameters) @@ -1170,52 +1342,52 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { initConfig.DataChecksums = db.Spec.NewConfig.DataChecksums } - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) return } - if err = pgm.RemoveAll(); err != nil { - log.Errorw("failed to remove the postgres data dir", zap.Error(err)) + if err = pgm.RemoveAllIfInitialized(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to remove the postgres data dir") return } - if err = pgm.Init(initConfig); err != nil { - log.Errorw("failed to initialize postgres database cluster", zap.Error(err)) + if err = pgm.Init(ctx, initConfig); err != nil { + logger.Error().AnErr("err", err).Msg("failed to initialize postgres database cluster") return } - if err = pgm.StartTmpMerged(); err != nil { - log.Errorw("failed to start instance", zap.Error(err)) + if err = pgm.StartTmpMerged(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start instance") return } - if err = pgm.WaitReady(cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { - log.Errorw("timeout waiting for instance to be ready", zap.Error(err)) + if err = pgm.WaitReady(ctx, cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { + logger.Error().AnErr("err", err).Msg("timeout waiting for instance to be ready") return } if db.Spec.IncludeConfig { - pgParameters, err = pgm.GetConfigFilePGParameters() + pgParameters, err = pgm.GetConfigFilePGParameters(ctx) if err != nil { - log.Errorw("failed to retrieve postgres parameters", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve postgres parameters") return } ndbls.InitPGParameters = pgParameters if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } } - log.Infow("setting roles") - if err = pgm.SetupRoles(); err != nil { - log.Errorw("failed to setup roles", zap.Error(err)) + logger.Info().Msg("setting roles") + if err = pgm.SetupRoles(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to setup roles") return } - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) return } - case cluster.DBInitModePITR: - log.Infow("restoring the database cluster") + case cluster.PITRDB: + logger.Info().Msg("restoring the database cluster") ndbls := &DBLocalState{ UID: db.UID, // Set a no generation since we aren't already converged. @@ -1223,26 +1395,26 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { Initializing: true, } if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } // create postgres parameters with empty InitPGParameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // update pgm postgres parameters pgm.SetParameters(pgParameters) - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgPgInst) return } - if err = pgm.RemoveAll(); err != nil { - log.Errorw("failed to remove the postgres data dir", zap.Error(err)) + if err = pgm.RemoveAllIfInitialized(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to remove the postgres data dir") return } - log.Infow("executing DataRestoreCommand") - if err = pgm.Restore(db.Spec.PITRConfig.DataRestoreCommand); err != nil { - log.Errorw("failed to restore postgres database cluster", zap.Error(err)) + logger.Info().Msg("executing DataRestoreCommand") + if err = pgm.Restore(ctx, db.Spec.PITRConfig.DataRestoreCommand); err != nil { + logger.Error().AnErr("err", err).Msg("failed to restore postgres database cluster") return } @@ -1253,47 +1425,51 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { standbySettings = db.Spec.FollowConfig.StandbySettings } - pgm.SetRecoveryOptions(p.createRecoveryOptions(recoveryMode, standbySettings, db.Spec.PITRConfig.ArchiveRecoverySettings, db.Spec.PITRConfig.RecoveryTargetSettings)) + pgm.SetRecoveryOptions(p.createRecoveryOptions( + recoveryMode, + standbySettings, + db.Spec.PITRConfig.ArchiveRecoverySettings, + db.Spec.PITRConfig.RecoveryTargetSettings)) - if err = pgm.StartTmpMerged(); err != nil { - log.Errorw("failed to start instance", zap.Error(err)) + if err = pgm.StartTmpMerged(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start instance") return } if recoveryMode == pg.RecoveryModeRecovery { - // wait for the db having replyed all the wals - log.Infof("waiting for recovery to be completed") + // wait for the db having replayed all the wals + logger.Info().Msg("waiting for recovery to be completed") if err = pgm.WaitRecoveryDone(cd.Cluster.DefSpec().SyncTimeout.Duration); err != nil { - log.Errorw("recovery not finished", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("recovery not finished") return } - log.Infof("recovery completed") + logger.Info().Msg("recovery completed") } - if err = pgm.WaitReady(cd.Cluster.DefSpec().SyncTimeout.Duration); err != nil { - log.Errorw("timeout waiting for instance to be ready", zap.Error(err)) + if err = pgm.WaitReady(ctx, cd.Cluster.DefSpec().SyncTimeout.Duration); err != nil { + logger.Error().AnErr("err", err).Msg("timeout waiting for instance to be ready") return } if db.Spec.IncludeConfig { - pgParameters, err = pgm.GetConfigFilePGParameters() + pgParameters, err = pgm.GetConfigFilePGParameters(ctx) if err != nil { - log.Errorw("failed to retrieve postgres parameters", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve postgres parameters") return } ndbls.InitPGParameters = pgParameters if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } } - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } - case cluster.DBInitModeResync: - log.Infow("resyncing the database cluster") + case cluster.ResyncDB: + logger.Info().Msg("resyncing the database cluster") ndbls := &DBLocalState{ // replace our current db uid with the required one. UID: db.UID, @@ -1302,27 +1478,27 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { Initializing: true, } if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } // create postgres parameters with empty InitPGParameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // update pgm postgres parameters pgm.SetParameters(pgParameters) var systemID string if !initialized { - log.Infow("database cluster not initialized") + logger.Info().Msg("database cluster not initialized") } else { - systemID, err = pgm.GetSystemdID() + systemID, err = pgm.GetSystemID() if err != nil { - log.Errorw("error retrieving systemd ID", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error retrieving systemd ID") return } } @@ -1330,7 +1506,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { followedUID := db.Spec.FollowConfig.DBUID followedDB, ok := cd.DBs[followedUID] if !ok { - log.Errorw("no db data available for followed db", "followedDB", followedUID) + logger.Error().Str(followedStr, followedUID).Msg("no db data available for followed db") return } @@ -1344,7 +1520,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { masterDB, ok := cd.DBs[cd.Cluster.Status.Master] if tryPgrewind && !ok { - log.Warn("no current master, disabling pg_rewind for this resync") + logger.Warn().Msg("no current master, disabling pg_rewind for this resync") tryPgrewind = false } @@ -1361,12 +1537,12 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { // wals and we'll force a full resync. // We have to find a better way to detect if a standby is waiting // for unavailable wals. - if err = p.resync(db, masterDB, followedDB, tryPgrewind); err != nil { - log.Errorw("failed to resync from followed instance", zap.Error(err)) + if err = p.resync(ctx, db, masterDB, followedDB, tryPgrewind); err != nil { + logger.Error().AnErr("err", err).Msg("failed to resync from followed instance") return } - if err = pgm.Start(); err != nil { - log.Errorw("failed to start instance", zap.Error(err)) + if err = pgm.Start(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start instance") return } @@ -1374,35 +1550,36 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { fullResync := false // if not accepting connection assume that it's blocked waiting for missing wal // (see above TODO), so do a full resync using pg_basebackup. - if err = pgm.WaitReady(cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { - log.Errorw("pg_rewinded standby is not accepting connection. it's probably waiting for unavailable wals. Forcing a full resync") + if err = pgm.WaitReady(ctx, cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { + // revive:disable-next-line + logger.Error().Msg("pg_rewinded standby is not accepting connection. it's probably waiting for unavailable wals. Forcing a full resync") fullResync = true } else { // Check again if it was really synced var pgState *cluster.PostgresState - pgState, err = p.GetPGState(pctx) + pgState, err = p.GetPGState(ctx) if err != nil { - log.Errorw("cannot get current pgstate", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("cannot get current pgstate") return } - if p.isDifferentTimelineBranch(followedDB, pgState) { + if p.isDifferentTimelineBranch(ctx, followedDB, pgState) { fullResync = true } } if fullResync { - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } - if err = p.resync(db, masterDB, followedDB, false); err != nil { - log.Errorw("failed to resync from followed instance", zap.Error(err)) + if err = p.resync(ctx, db, masterDB, followedDB, false); err != nil { + logger.Error().AnErr("err", err).Msg("failed to resync from followed instance") return } } } - case cluster.DBInitModeExisting: + case cluster.ExistingDB: ndbls := &DBLocalState{ // replace our current db uid with the required one. UID: db.UID, @@ -1411,55 +1588,56 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { Initializing: false, } if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } // create postgres parameters with empty InitPGParameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // update pgm postgres parameters pgm.SetParameters(pgParameters) - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } - if err = pgm.StartTmpMerged(); err != nil { - log.Errorw("failed to start instance", zap.Error(err)) + if err = pgm.StartTmpMerged(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start instance") return } - if err = pgm.WaitReady(cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { - log.Errorw("timeout waiting for instance to be ready", zap.Error(err)) + if err = pgm.WaitReady(ctx, cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { + logger.Error().AnErr("err", err).Msg("timeout waiting for instance to be ready") return } if db.Spec.IncludeConfig { - pgParameters, err = pgm.GetConfigFilePGParameters() + pgParameters, err = pgm.GetConfigFilePGParameters(ctx) if err != nil { - log.Errorw("failed to retrieve postgres parameters", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve postgres parameters") return } ndbls.InitPGParameters = pgParameters if err = p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } } - if err = pgm.StopIfStarted(true); err != nil { - log.Errorw("failed to stop pg instance", zap.Error(err)) + if err = pgm.StopIfStarted(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } - case cluster.DBInitModeNone: - log.Errorw("different local dbUID but init mode is none, this shouldn't happen. Something bad happened to the keeper data. Check that keeper data is on a persistent volume and that the keeper state files weren't removed") + case cluster.NoDB: + // revive:disable-next-line + logger.Error().Msg("different local dbUID but init mode is none, this shouldn't happen. Something bad happened to the keeper data. Check that keeper data is on a persistent volume and that the keeper state files weren't removed") return default: - log.Errorw("unknown db init mode", "initMode", string(db.Spec.InitMode)) + logger.Error().Str("initMode", string(db.Spec.InitMode)).Msg("unknown db init mode") return } } initialized, err := pgm.IsInitialized() if err != nil { - log.Errorw("failed to detect if instance is initialized", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to detect if instance is initialized") return } @@ -1468,43 +1646,43 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { started, err = pgm.IsStarted() if err != nil { // log error getting instance state but go ahead. - log.Errorw("failed to retrieve instance status", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve instance status") } - log.Debugw("db status", "initialized", true, "started", started) + logger.Debug().Bool("initialized", true).Bool("started", started).Msg("db status") } else { - log.Debugw("db status", "initialized", false, "started", false) + logger.Debug().Bool("initialized", false).Bool("started", false).Msg("db status") } // create postgres parameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // update pgm postgres parameters pgm.SetParameters(pgParameters) var localRole common.Role if !initialized { - log.Infow("database cluster not initialized") + logger.Info().Msg("database cluster not initialized") localRole = common.RoleUndefined } else { localRole, err = pgm.GetRole() if err != nil { - log.Errorw("error retrieving current pg role", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error retrieving current pg role") return } } targetRole := db.Spec.Role - log.Debugw("target role", "targetRole", string(targetRole)) + logger.Debug().Str("targetRole", string(targetRole)).Msg("target role") // Set metrics to power alerts about mismatched roles setRole(localRoleGauge, &localRole) setRole(targetRoleGauge, &targetRole) switch targetRole { - case common.RoleMaster: + case common.RolePrimary: // We are the elected master - log.Infow("our db requested role is master") + logger.Info().Msg("our db requested role is master") if localRole == common.RoleUndefined { - log.Errorw("database cluster not initialized but requested role is master. This shouldn't happen!") + logger.Error().Msg("database cluster not initialized but requested role is master. This shouldn't happen!") return } @@ -1512,77 +1690,81 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { started, err := pgm.IsStarted() if err != nil { - log.Errorw("failed to retrieve instance status", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve instance status") return } if !started { - // if we have syncrepl enabled and the postgres instance is stopped, before opening connections to normal users wait for having the defined synchronousStandbys in sync state. + // if we have syncrepl enabled and the postgres instance is stopped, before opening connections to normal + // users wait for having the defined synchronousStandbys in sync state. if db.Spec.SynchronousReplication { p.waitSyncStandbysSynced = true - log.Infow("not allowing connection as normal users since synchronous replication is enabled and instance was down") - pgm.SetHba(p.generateHBA(cd, db, true)) + // revive:disable-next-line + logger.Info().Msg("not allowing connection as normal users since synchronous replication is enabled and instance was down") + pgm.SetHba(p.generateHBA(ctx, cd, db, true)) } - if err = pgm.Start(); err != nil { - log.Errorw("failed to start postgres", zap.Error(err)) + if err = pgm.Start(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start postgres") return } - if err = pgm.WaitReady(cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { - log.Errorw("timeout waiting for instance to be ready", zap.Error(err)) + if err = pgm.WaitReady(ctx, cd.Cluster.DefSpec().DBWaitReadyTimeout.Duration); err != nil { + logger.Error().AnErr("err", err).Msg("timeout waiting for instance to be ready") return } } - if localRole == common.RoleStandby { - log.Infow("promoting to master") - if err = pgm.Promote(); err != nil { - log.Errorw("failed to promote instance", zap.Error(err)) + if localRole == common.RoleReplica { + logger.Info().Msg("promoting to master") + if err = pgm.Promote(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to promote instance") return } } else { - log.Infow("already master") + logger.Info().Msg("already master") } - if err := p.refreshReplicationSlots(cd, db); err != nil { - log.Errorw("error updating replication slots", zap.Error(err)) + if err := p.refreshReplicationSlots(ctx, cd, db); err != nil { + logger.Error().AnErr("err", err).Msg("error updating replication slots") return } - case common.RoleStandby: + case common.RoleReplica: // We are a standby var standbySettings *cluster.StandbySettings switch db.Spec.FollowConfig.Type { case cluster.FollowTypeInternal: followedUID := db.Spec.FollowConfig.DBUID - log.Infow("our db requested role is standby", "followedDB", followedUID) + logger.Info().Str(followedStr, followedUID).Msg("our db requested role is standby") followedDB, ok := cd.DBs[followedUID] if !ok { - log.Errorw("no db data available for followed db", "followedDB", followedUID) + logger.Error().Str(followedStr, followedUID).Msg("no db data available for followed db") return } replConnParams := p.getReplConnParams(db, followedDB) - standbySettings = &cluster.StandbySettings{PrimaryConninfo: replConnParams.ConnString(), PrimarySlotName: common.StolonName(db.UID)} + standbySettings = &cluster.StandbySettings{ + PrimaryConninfo: replConnParams.ConnString(), + PrimarySlotName: common.StolonName(db.UID)} case cluster.FollowTypeExternal: standbySettings = db.Spec.FollowConfig.StandbySettings default: - log.Errorw("unknown follow type", "followType", string(db.Spec.FollowConfig.Type)) + logger.Error().Str("followType", string(db.Spec.FollowConfig.Type)).Msg("unknown follow type") return } switch localRole { - case common.RoleMaster: - log.Errorw("cannot move from master role to standby role") + case common.RolePrimary: + logger.Error().Msg("cannot move from master role to standby role") return - case common.RoleStandby: - log.Infow("already standby") + case common.RoleReplica: + logger.Info().Msg("already standby") started, err := pgm.IsStarted() if err != nil { - log.Errorw("failed to retrieve instance status", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve instance status") return } if !started { pgm.SetRecoveryOptions(p.createRecoveryOptions(pg.RecoveryModeStandby, standbySettings, nil, nil)) - if err = pgm.Start(); err != nil { - log.Errorw("failed to start postgres", zap.Error(err)) + if err = pgm.Start(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to start postgres") return } } @@ -1593,74 +1775,91 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { followedUID := db.Spec.FollowConfig.DBUID followedDB, ok := cd.DBs[followedUID] if !ok { - log.Errorw("no db data available for followed db", "followedDB", followedUID) + logger.Error().Str(followedStr, followedUID).Msg("no db data available for followed db") return } newReplConnParams := p.getReplConnParams(db, followedDB) - log.Debugw("newReplConnParams", "newReplConnParams", newReplConnParams) + logger.Debug().Any("newReplConnParams", newReplConnParams).Msg("newReplConnParams") - standbySettings := &cluster.StandbySettings{PrimaryConninfo: newReplConnParams.ConnString(), PrimarySlotName: common.StolonName(db.UID)} + standbySettings := &cluster.StandbySettings{ + PrimaryConninfo: newReplConnParams.ConnString(), + PrimarySlotName: common.StolonName(db.UID)} curRecoveryOptions := pgm.CurRecoveryOptions() newRecoveryOptions := p.createRecoveryOptions(pg.RecoveryModeStandby, standbySettings, nil, nil) // Update recovery conf if parameters has changed if !curRecoveryOptions.RecoveryParameters.Equals(newRecoveryOptions.RecoveryParameters) { - log.Infow("recovery parameters changed, restarting postgres instance", "curRecoveryParameters", curRecoveryOptions.RecoveryParameters, "newRecoveryParameters", newRecoveryOptions.RecoveryParameters) + logger.Info(). + Any("curRecoveryParameters", curRecoveryOptions.RecoveryParameters). + Any("newRecoveryParameters", newRecoveryOptions.RecoveryParameters). + Msg("recovery parameters changed, restarting postgres instance") pgm.SetRecoveryOptions(newRecoveryOptions) - if err = pgm.Restart(true); err != nil { - log.Errorw("failed to restart postgres instance", zap.Error(err)) + if err = pgm.Restart(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg("failed to restart postgres instance") return } } - if err = p.refreshReplicationSlots(cd, db); err != nil { - log.Errorw("error updating replication slots", zap.Error(err)) + if err = p.refreshReplicationSlots(ctx, cd, db); err != nil { + logger.Error().AnErr("err", err).Msg("error updating replication slots") } case cluster.FollowTypeExternal: curRecoveryOptions := pgm.CurRecoveryOptions() - newRecoveryOptions := p.createRecoveryOptions(pg.RecoveryModeStandby, db.Spec.FollowConfig.StandbySettings, db.Spec.FollowConfig.ArchiveRecoverySettings, nil) + newRecoveryOptions := p.createRecoveryOptions( + pg.RecoveryModeStandby, + db.Spec.FollowConfig.StandbySettings, + db.Spec.FollowConfig.ArchiveRecoverySettings, + nil) // Update recovery conf if parameters has changed if !curRecoveryOptions.RecoveryParameters.Equals(newRecoveryOptions.RecoveryParameters) { - log.Infow("recovery parameters changed, restarting postgres instance", "curRecoveryParameters", curRecoveryOptions.RecoveryParameters, "newRecoveryParameters", newRecoveryOptions.RecoveryParameters) + logger.Info(). + Any("curRecoveryParameters", curRecoveryOptions.RecoveryParameters). + Any("newRecoveryParameters", newRecoveryOptions.RecoveryParameters). + Msg("recovery parameters changed, restarting postgres instance") pgm.SetRecoveryOptions(newRecoveryOptions) - if err = pgm.Restart(true); err != nil { - log.Errorw("failed to restart postgres instance", zap.Error(err)) + if err = pgm.Restart(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg("failed to restart postgres instance") return } } - if err = p.refreshReplicationSlots(cd, db); err != nil { - log.Errorw("error updating replication slots", zap.Error(err)) + if err = p.refreshReplicationSlots(ctx, cd, db); err != nil { + logger.Error().AnErr("err", err).Msg("error updating replication slots") } } case common.RoleUndefined: - log.Infow("our db role is none") + logger.Info().Msg("our db role is none") return } case common.RoleUndefined: - log.Infow("our db requested role is none") + logger.Info().Msg("our db requested role is none") return } // update pg parameters - pgParameters = p.createPGParameters(db) + pgParameters = p.createPGParameters(ctx, db) // Log synchronous replication changes prevSyncStandbyNames := pgm.CurParameters()["synchronous_standby_names"] syncStandbyNames := pgParameters["synchronous_standby_names"] if db.Spec.SynchronousReplication { if prevSyncStandbyNames != syncStandbyNames { - log.Infow("needed synchronous_standby_names changed", "prevSyncStandbyNames", prevSyncStandbyNames, "syncStandbyNames", syncStandbyNames) + logger.Info(). + Str("prevSyncStandbyNames", prevSyncStandbyNames). + Str("syncStandbyNames", syncStandbyNames). + Msg("needed synchronous_standby_names changed") } } else { if prevSyncStandbyNames != "" { - log.Infow("sync replication disabled, removing current synchronous_standby_names", "syncStandbyNames", prevSyncStandbyNames) + logger.Info(). + Str("syncStandbyNames", prevSyncStandbyNames). + Msg("sync replication disabled, removing current synchronous_standby_names") } } @@ -1668,45 +1867,47 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { changedParams := pgParameters.Diff(pgm.CurParameters()) if !pgParameters.Equals(pgm.CurParameters()) { - log.Infow("postgres parameters changed, reloading postgres instance") + logger.Info().Msg("postgres parameters changed, reloading postgres instance") pgm.SetParameters(pgParameters) needsReload = true } else { // for tests - log.Infow("postgres parameters not changed") + logger.Info().Msg("postgres parameters not changed") } // Generate hba auth from clusterData - // if we have syncrepl enabled and the postgres instance is stopped, before opening connections to normal users wait for having the defined synchronousStandbys in sync state. + // if we have syncrepl enabled and the postgres instance is stopped, before opening connections to normal users + // wait for having the defined synchronousStandbys in sync state. if db.Spec.SynchronousReplication && p.waitSyncStandbysSynced { - inSyncStandbys, err := p.GetInSyncStandbys() + inSyncStandbys, err := p.GetInSyncStandbys(ctx) if err != nil { - log.Errorw("failed to retrieve current in sync standbys from instance", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to retrieve current in sync standbys from instance") return } if !util.CompareStringSliceNoOrder(inSyncStandbys, db.Spec.SynchronousStandbys) { - log.Infow("not allowing connection as normal users since synchronous replication is enabled, instance was down and not all sync standbys are synced") + // revive:disable-next-line + logger.Info().Msg("not allowing connection as normal users since synchronous replication is enabled, instance was down and not all sync standbys are synced") } else { p.waitSyncStandbysSynced = false } } else { p.waitSyncStandbysSynced = false } - newHBA := p.generateHBA(cd, db, p.waitSyncStandbysSynced) + newHBA := p.generateHBA(ctx, cd, db, p.waitSyncStandbysSynced) if !reflect.DeepEqual(newHBA, pgm.CurHba()) { - log.Infow("postgres hba entries changed, reloading postgres instance") + logger.Info().Msg("postgres hba entries changed, reloading postgres instance") pgm.SetHba(newHBA) needsReload = true } else { // for tests - log.Infow("postgres hba entries not changed") + logger.Info().Msg("postgres hba entries not changed") } if needsReload { needsReloadGauge.Set(1) // mark as reload needed - if err := pgm.Reload(); err != nil { - log.Errorw("failed to reload postgres instance", err) + if err := pgm.Reload(ctx); err != nil { + logger.Error().AnErr("err", err).Msg("failed to reload postgres instance") } else { needsReloadGauge.Set(0) // successful reload implies no longer required } @@ -1716,17 +1917,17 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { clusterSpec := cd.Cluster.DefSpec() automaticPgRestartEnabled := *clusterSpec.AutomaticPgRestart - needsRestart, err := pgm.IsRestartRequired(changedParams) + needsRestart, err := pgm.IsRestartRequired(ctx, changedParams) if err != nil { - log.Errorw("failed to check if restart is required", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to check if restart is required") } if needsRestart { needsRestartGauge.Set(1) // mark as restart needed if automaticPgRestartEnabled { - log.Infow("restarting postgres") - if err := pgm.Restart(true); err != nil { - log.Errorw("failed to restart postgres instance", zap.Error(err)) + logger.Info().Msg("restarting postgres") + if err := pgm.Restart(ctx, true); err != nil { + logger.Error().AnErr("err", err).Msg("failed to restart postgres instance") } else { needsRestartGauge.Set(0) // successful restart implies no longer required } @@ -1739,7 +1940,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) { ndbls.Generation = db.Generation ndbls.Initializing = false if err := p.saveDBLocalState(ndbls); err != nil { - log.Errorw("failed to save db local state", zap.Error(err)) + logger.Error().AnErr("err", err).Msg(errorMsgDbState) return } @@ -1753,7 +1954,7 @@ func (p *PostgresKeeper) keeperLocalStateFilePath() string { } func (p *PostgresKeeper) loadKeeperLocalState() error { - sj, err := ioutil.ReadFile(p.keeperLocalStateFilePath()) + sj, err := os.ReadFile(p.keeperLocalStateFilePath()) if err != nil { return err } @@ -1770,7 +1971,7 @@ func (p *PostgresKeeper) saveKeeperLocalState() error { if err != nil { return err } - return common.WriteFileAtomic(p.keeperLocalStateFilePath(), 0600, sj) + return common.WriteFileAtomic(p.keeperLocalStateFilePath(), ownerRWPermisions, sj) } func (p *PostgresKeeper) dbLocalStateFilePath() string { @@ -1778,7 +1979,7 @@ func (p *PostgresKeeper) dbLocalStateFilePath() string { } func (p *PostgresKeeper) loadDBLocalState() error { - sj, err := ioutil.ReadFile(p.dbLocalStateFilePath()) + sj, err := os.ReadFile(p.dbLocalStateFilePath()) if err != nil { return err } @@ -1797,7 +1998,7 @@ func (p *PostgresKeeper) saveDBLocalState(dbls *DBLocalState) error { if err != nil { return err } - if err = common.WriteFileAtomic(p.dbLocalStateFilePath(), 0600, sj); err != nil { + if err = common.WriteFileAtomic(p.dbLocalStateFilePath(), ownerRWPermisions, sj); err != nil { return err } @@ -1815,9 +2016,9 @@ func (p *PostgresKeeper) saveDBLocalState(dbls *DBLocalState) error { // * Has a standby db role with followtype external func IsMaster(db *cluster.DB) bool { switch db.Spec.Role { - case common.RoleMaster: + case common.RolePrimary: return true - case common.RoleStandby: + case common.RoleReplica: if db.Spec.FollowConfig.Type == cluster.FollowTypeExternal { return true } @@ -1827,18 +2028,30 @@ func IsMaster(db *cluster.DB) bool { } } +// localAuthMethod returns the authentication method that works for local connections +// cert does not work for local connections, in which case we should fall back to peer authentication +func localAuthMethod(ctx context.Context, authMethod string) string { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) + if authMethod == authCert { + logger.Info().Msg("using peer instead of cert for local connection authentication method") + return authPeer + } + return authMethod +} + // generateHBA generates the instance hba entries depending on the value of // DefaultSUReplAccessMode. // When onlyInternal is true only rules needed for replication will be setup // and the traffic should be permitted only for pgSUUsername standard // connections and pgReplUsername replication connections. -func (p *PostgresKeeper) generateHBA(cd *cluster.ClusterData, db *cluster.DB, onlyInternal bool) []string { +func (p *PostgresKeeper) generateHBA(ctx context.Context, cd *cluster.Data, db *cluster.DB, + onlyInternal bool) []string { // Minimal entries for local normal and replication connections needed by the stolon keeper - // Matched local connections are for postgres database and suUsername user with md5 auth - // Matched local replication connections are for replUsername user with md5 auth + // Matched local connections are for postgres database and suUsername user with authMd5 auth + // Matched local replication connections are for replUsername user with authMd5 auth computedHBA := []string{ - fmt.Sprintf("local postgres %s %s", p.pgSUUsername, p.pgSUAuthMethod), - fmt.Sprintf("local replication %s %s", p.pgReplUsername, p.pgReplAuthMethod), + fmt.Sprintf("local postgres %s %s", p.pgSUUsername, localAuthMethod(ctx, p.pgSULocalAuthMethod)), + fmt.Sprintf("local replication %s %s", p.pgReplUsername, localAuthMethod(ctx, p.pgReplLocalAuthMethod)), } switch *cd.Cluster.DefSpec().DefaultSUReplAccessMode { @@ -1846,13 +2059,14 @@ func (p *PostgresKeeper) generateHBA(cd *cluster.ClusterData, db *cluster.DB, on // all the keepers will accept connections from every host computedHBA = append( computedHBA, - fmt.Sprintf("host all %s %s %s", p.pgSUUsername, "0.0.0.0/0", p.pgSUAuthMethod), - fmt.Sprintf("host all %s %s %s", p.pgSUUsername, "::0/0", p.pgSUAuthMethod), - fmt.Sprintf("host replication %s %s %s", p.pgReplUsername, "0.0.0.0/0", p.pgReplAuthMethod), - fmt.Sprintf("host replication %s %s %s", p.pgReplUsername, "::0/0", p.pgReplAuthMethod), + fmt.Sprintf("%s all %s %s %s", p.pgSUConnType, p.pgSUUsername, "0.0.0.0/0", p.pgSUAuthMethod), + fmt.Sprintf("%s all %s %s %s", p.pgSUConnType, p.pgSUUsername, "::0/0", p.pgSUAuthMethod), + fmt.Sprintf("%s replication %s %s %s", p.pgReplConnType, p.pgReplUsername, "0.0.0.0/0", p.pgReplAuthMethod), + fmt.Sprintf("%s replication %s %s %s", p.pgReplConnType, p.pgReplUsername, "::0/0", p.pgReplAuthMethod), ) case cluster.SUReplAccessStrict: - // only the master keeper (primary instance or standby of a remote primary when in standby cluster mode) will accept connections only from the other standby keepers IPs + // only the master keeper (primary instance or standby of a remote primary when in standby cluster mode) will + // accept connections only from the other standby keepers IPs if IsMaster(db) { addresses := []string{} for _, dbElt := range cd.DBs { @@ -1864,8 +2078,18 @@ func (p *PostgresKeeper) generateHBA(cd *cluster.ClusterData, db *cluster.DB, on for _, address := range addresses { computedHBA = append( computedHBA, - fmt.Sprintf("host all %s %s/32 %s", p.pgSUUsername, address, p.pgReplAuthMethod), - fmt.Sprintf("host replication %s %s/32 %s", p.pgReplUsername, address, p.pgReplAuthMethod), + fmt.Sprintf( + "%s all %s %s/32 %s", + p.pgSUConnType, + p.pgSUUsername, + address, + p.pgReplAuthMethod), + fmt.Sprintf( + "%s replication %s %s/32 %s", + p.pgReplConnType, + p.pgReplUsername, + address, + p.pgReplAuthMethod), ) } } @@ -1873,14 +2097,14 @@ func (p *PostgresKeeper) generateHBA(cd *cluster.ClusterData, db *cluster.DB, on if !onlyInternal { // By default, if no custom pg_hba entries are provided, accept - // connections for all databases and users with md5 auth + // connections for all databases and users with authMd5 auth if db.Spec.PGHBA != nil { computedHBA = append(computedHBA, db.Spec.PGHBA...) } else { computedHBA = append( computedHBA, - "host all all 0.0.0.0/0 md5", - "host all all ::0/0 md5", + fmt.Sprintf("%s all all 0.0.0.0/0 %s", p.pgSUConnType, p.pgSUAuthMethod), + fmt.Sprintf("%s all all ::0/0 %s", p.pgSUConnType, p.pgSUAuthMethod), ) } } @@ -1889,24 +2113,28 @@ func (p *PostgresKeeper) generateHBA(cd *cluster.ClusterData, db *cluster.DB, on return computedHBA } -func sigHandler(sigs chan os.Signal, cancel context.CancelFunc) { +func sigHandler(ctx context.Context, sigs chan os.Signal, cancel context.CancelFunc) { + _, logger := logging.GetLogComponent(ctx, logging.KeeperComponent) s := <-sigs - log.Debugw("got signal", "signal", s) + logger.Debug().Any("signal", s).Msg("got signal") shutdownSeconds.SetToCurrentTime() cancel() } +// Execute is the main executor of keeper file func Execute() { + _, logger := logging.GetLogComponent(context.Background(), logging.KeeperComponent) if err := flagutil.SetFlagsFromEnv(CmdKeeper.PersistentFlags(), "STKEEPER"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } if err := CmdKeeper.Execute(); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } } -func keeper(c *cobra.Command, args []string) { +func keeper(c *cobra.Command, _ []string) { + ctx, logger := logging.GetLogComponent(context.Background(), logging.KeeperComponent) var ( err error listenAddFlag = "pg-advertise-address" @@ -1919,50 +2147,47 @@ func keeper(c *cobra.Command, args []string) { var user string user, err = util.GetUser() if err != nil { - log.Fatalf("cannot get current user: %v", err) + logger.Fatal().AnErr("err", err).Msg("cannot get current user") } cfg.pgSUUsername = user } - validAuthMethods := make(map[string]struct{}) - validAuthMethods["trust"] = struct{}{} - validAuthMethods["md5"] = struct{}{} - switch cfg.LogLevel { - case "error": - slog.SetLevel(zap.ErrorLevel) - case "warn": - slog.SetLevel(zap.WarnLevel) - case "info": - slog.SetLevel(zap.InfoLevel) - case "debug": - slog.SetLevel(zap.DebugLevel) - default: - log.Fatalf("invalid log level: %v", cfg.LogLevel) - } + validAuthMethods := map[string]struct{}{} + validAuthMethods[authTrust] = struct{}{} + validAuthMethods[authMd5] = struct{}{} + validAuthMethods[authCert] = struct{}{} + validAuthMethods[authIdent] = struct{}{} + validAuthMethods[authPeer] = struct{}{} + validConnectionTypes := map[string]struct{}{} + validConnectionTypes[connTypeHost] = struct{}{} + validConnectionTypes[connTypeHostSsl] = struct{}{} + validConnectionTypes[connTypeHostNoSsl] = struct{}{} + validConnectionTypes[connTypeHostGssEnc] = struct{}{} + validConnectionTypes[connTypeHostNoGssEnc] = struct{}{} + logging.SetStaticLevel(cfg.LogLevel) if cfg.debug { - slog.SetDebug() + logging.SetStaticLevel("debug") } if cmd.IsColorLoggerEnable(c, &cfg.CommonConfig) { - log = slog.SColor() - pg.SetLogger(log) + logging.EnableColor() } if cfg.dataDir == "" { - log.Fatalf("data dir required") + logger.Fatal().Msg("data dir required") } if err = cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { - log.Fatalf(err.Error()) + logger.Fatal().Msg(err.Error()) } cmd.SetMetrics(&cfg.CommonConfig, "keeper") - if err = os.MkdirAll(cfg.dataDir, 0700); err != nil { - log.Fatalf("cannot create data dir: %v", err) + if err = os.MkdirAll(cfg.dataDir, ownerRWExecPermisions); err != nil { + logger.Fatal().AnErr("err", err).Msg("cannot create data dir") } if cfg.pgListenAddress == "" { - log.Fatalf("--pg-listen-address is required") + logger.Fatal().Msg("--pg-listen-address is required") } if cfg.pgAdvertiseAddress == "" { @@ -1976,103 +2201,155 @@ func keeper(c *cobra.Command, args []string) { ip := net.ParseIP(cfg.pgAdvertiseAddress) if ip == nil { - log.Warnf("provided --%s %q: is not an ip address but a hostname. This will be advertized to the other components and may have undefined behaviors if resolved differently by other hosts", listenAddFlag, cfg.pgAdvertiseAddress) + logger.Warn(). + Str("flag", listenAddFlag). + Str("value", cfg.pgAdvertiseAddress). + Msg("provided flag is not an ip address but a hostname. " + + "This will be advertized to the other components and may have undefined behaviors " + + "if resolved differently by other hosts") } ipAddr, err := net.ResolveIPAddr("ip", cfg.pgAdvertiseAddress) if err != nil { - log.Warnf("cannot resolve provided --%s %q: %v", listenAddFlag, cfg.pgAdvertiseAddress, err) + logger.Warn(). + AnErr("err", err). + Str("flag", listenAddFlag). + Str("address", cfg.pgAdvertiseAddress). + Msg("cannot resolve provided flag") } else { if ipAddr.IP.IsLoopback() { - log.Warnf("provided --%s %q is a loopback ip. This will be advertized to the other components and communication will fail if they are on different hosts", listenAddFlag, cfg.pgAdvertiseAddress) + logger.Warn(). + Str("flag", listenAddFlag). + Str("address", cfg.pgAdvertiseAddress). + Msg("provided address for flag is a loopback ip. " + + "This will be advertized to the other components " + + "and communication will fail if they are on different hosts") } } + if cfg.pgSULocalAuthMethod == "" { + cfg.pgSULocalAuthMethod = cfg.pgSUAuthMethod + } + if cfg.pgReplLocalAuthMethod == "" { + cfg.pgReplLocalAuthMethod = cfg.pgReplAuthMethod + } + if _, ok := validConnectionTypes[cfg.pgReplConnType]; !ok { + logger.Fatal(). + Msg("--pg-repl-connection-type must be one of: host, hostssl, hostnossl, hostgssenc, hostnogssenc") + } + if _, ok := validConnectionTypes[cfg.pgSUConnType]; !ok { + logger.Fatal(). + Msg("--pg-su-connection-type must be one of: host, hostssl, hostnossl, hostgssenc, hostnogssenc") + } if _, ok := validAuthMethods[cfg.pgReplAuthMethod]; !ok { - log.Fatalf("--pg-repl-auth-method must be one of: md5, trust") + logger.Fatal(). + Msg("--pg-repl-auth-method must be one of: ident, authMd5, password, trust or cert") } if cfg.pgReplUsername == "" { - log.Fatalf("--pg-repl-username is required") + logger.Fatal(). + Msg("--pg-repl-username is required") + } + if _, ok := validAuthMethods[cfg.pgReplLocalAuthMethod]; !ok { + logger.Fatal(). + Msg("--pg-repl-local-auth-method must be one of: ident, authMd5, password, trust or cert") } - if cfg.pgReplAuthMethod == "trust" { - log.Warn("not utilizing a password for replication between hosts is extremely dangerous") + if cfg.pgReplAuthMethod == authTrust { + logger.Warn(). + Msg("not utilizing a password for replication between hosts is extremely dangerous") if cfg.pgReplPassword != "" || cfg.pgReplPasswordFile != "" { - log.Fatalf("can not utilize --pg-repl-auth-method trust together with --pg-repl-password or --pg-repl-passwordfile") + // revive:disable-next-line + logger.Fatal(). + Msg("can not utilize --pg-repl-auth-method trust together with --pg-repl-password " + + "or --pg-repl-passwordfile") } } - if cfg.pgSUAuthMethod == "trust" { - log.Warn("not utilizing a password for superuser is extremely dangerous") - if cfg.pgSUPassword != "" || cfg.pgSUPasswordFile != "" { - log.Fatalf("can not utilize --pg-su-auth-method trust together with --pg-su-password or --pg-su-passwordfile") + if cfg.pgSUAuthMethod == authTrust || cfg.pgSULocalAuthMethod == authTrust { + logger.Warn().Msg("not utilizing a password for superuser is extremely dangerous") + if (cfg.pgSUAuthMethod == authTrust || + cfg.pgSULocalAuthMethod == authTrust) && (cfg.pgSUPassword != "" || + cfg.pgSUPasswordFile != "") { + // revive:disable-next-line + logger.Fatal().Msg("can not utilize --pg-su-auth-method trust and --pg-su-auth-method trust together with --pg-su-password or --pg-su-passwordfile") } } - if cfg.pgReplAuthMethod != "trust" && cfg.pgReplPassword == "" && cfg.pgReplPasswordFile == "" { - log.Fatalf("one of --pg-repl-password or --pg-repl-passwordfile is required") + if cfg.pgReplAuthMethod == authMd5 && cfg.pgReplPassword == "" && cfg.pgReplPasswordFile == "" { + logger.Fatal().Msg("one of --pg-repl-password or --pg-repl-passwordfile is required") } - if cfg.pgReplAuthMethod != "trust" && cfg.pgReplPassword != "" && cfg.pgReplPasswordFile != "" { - log.Fatalf("only one of --pg-repl-password or --pg-repl-passwordfile must be provided") + if cfg.pgReplAuthMethod == authMd5 && cfg.pgReplPassword != "" && cfg.pgReplPasswordFile != "" { + logger.Fatal().Msg("only one of --pg-repl-password or --pg-repl-passwordfile must be provided") } if _, ok := validAuthMethods[cfg.pgSUAuthMethod]; !ok { - log.Fatalf("--pg-su-auth-method must be one of: md5, password, trust") + logger.Fatal().Msg("--pg-su-auth-method must be one of: ident, authMd5, password, trust or cert") } - if cfg.pgSUAuthMethod != "trust" && cfg.pgSUPassword == "" && cfg.pgSUPasswordFile == "" { - log.Fatalf("one of --pg-su-password or --pg-su-passwordfile is required") + if cfg.pgSUAuthMethod == authMd5 && cfg.pgSUPassword == "" && cfg.pgSUPasswordFile == "" { + logger.Fatal().Msg("one of --pg-su-password or --pg-su-passwordfile is required") } - if cfg.pgSUAuthMethod != "trust" && cfg.pgSUPassword != "" && cfg.pgSUPasswordFile != "" { - log.Fatalf("only one of --pg-su-password or --pg-su-passwordfile must be provided") + if cfg.pgSUAuthMethod == authMd5 && cfg.pgSUPassword != "" && cfg.pgSUPasswordFile != "" { + logger.Fatal().Msg("only one of --pg-su-password or --pg-su-passwordfile must be provided") + } + if _, ok := validAuthMethods[cfg.pgSULocalAuthMethod]; !ok { + logger.Fatal(). + Msg("--pg-su-local-auth-method must be one of: ident, authMd5, password, trust or cert") } if cfg.pgReplPasswordFile != "" { - cfg.pgReplPassword, err = readPasswordFromFile(cfg.pgReplPasswordFile) + cfg.pgReplPassword, err = readPasswordFromFile(ctx, cfg.pgReplPasswordFile) if err != nil { - log.Fatalf("cannot read pg replication user password: %v", err) + logger.Fatal(). + AnErr("err", err). + Msg("cannot read pg replication user password") } } if cfg.pgSUPasswordFile != "" { - cfg.pgSUPassword, err = readPasswordFromFile(cfg.pgSUPasswordFile) + cfg.pgSUPassword, err = readPasswordFromFile(ctx, cfg.pgSUPasswordFile) if err != nil { - log.Fatalf("cannot read pg superuser password: %v", err) + logger.Fatal().AnErr("err", err).Msg("cannot read pg superuser password") } } // Trim trailing new lines from passwords tp := strings.TrimRight(cfg.pgSUPassword, "\r\n") if cfg.pgSUPassword != tp { - log.Warn("superuser password contain trailing new line, removing") + logger.Warn().Msg("superuser password contain trailing new line, removing") if tp == "" { - log.Fatalf("superuser password is empty after removing trailing new line") + logger.Fatal().Msg("superuser password is empty after removing trailing new line") } cfg.pgSUPassword = tp } tp = strings.TrimRight(cfg.pgReplPassword, "\r\n") if cfg.pgReplPassword != tp { - log.Warn("replication user password contain trailing new line, removing") + logger.Warn().Msg("replication user password contain trailing new line, removing") if tp == "" { - log.Fatalf("replication user password is empty after removing trailing new line") + logger.Fatal().Msg("replication user password is empty after removing trailing new line") } cfg.pgReplPassword = tp } if cfg.pgSUUsername == cfg.pgReplUsername { - log.Warn("superuser name and replication user name are the same. Different users are suggested.") + logger.Warn(). + Msg("superuser name and replication user name are the same. Different users are suggested.") if cfg.pgReplAuthMethod != cfg.pgSUAuthMethod { - log.Fatalf("do not support different auth methods when utilizing superuser for replication.") + logger.Fatal().Msg("do not support different auth methods when utilizing superuser for replication.") } - if cfg.pgSUPassword != cfg.pgReplPassword && cfg.pgSUAuthMethod != "trust" && cfg.pgReplAuthMethod != "trust" { - log.Fatalf("provided superuser name and replication user name are the same but provided passwords are different") + if cfg.pgSUPassword != cfg.pgReplPassword && cfg.pgSUAuthMethod == authMd5 && cfg.pgReplAuthMethod == authMd5 { + // revive:disable-next-line + logger.Fatal().Msg("provided superuser name and replication user name are the same but provided passwords are different") } } // Open (and create if needed) the lock file. // There is no need to clean up this file since we don't use the file as an actual lock. We get a lock - // on the file. So the lock get released when our process stops (or log.Fatalfs). + // on the file. So the lock get released when our process stops (or logger.Fatal). var lockFile *os.File if !cfg.disableDataDirLocking { lockFileName := filepath.Join(cfg.dataDir, "lock") - lockFile, err = os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0644) + lockFile, err = os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, ownerRWOtherRPermisions) if err != nil { - log.Fatalf("cannot take exclusive lock on data dir %q: %v", lockFileName, err) + logger.Fatal(). + Str("logfile", lockFileName). + AnErr("err", err). + Msg("cannot take exclusive lock on data dir") } // Get a lock on our lock file. @@ -2085,15 +2362,22 @@ func keeper(c *cobra.Command, args []string) { err = syscall.FcntlFlock(lockFile.Fd(), syscall.F_SETLK, ft) if err != nil { - log.Fatalf("cannot take exclusive lock on data dir %q: %v", lockFileName, err) + logger.Fatal(). + Str("logfile", lockFileName). + AnErr("err", err). + Msg("cannot take exclusive lock on data dir") } - log.Infow("exclusive lock on data dir taken") + logger.Info().Msg("exclusive lock on data dir taken") } if cfg.uid != "" { if !pg.IsValidReplSlotName(cfg.uid) { - log.Fatalf("keeper uid %q not valid. It can contain only lower-case letters, numbers and the underscore character", cfg.uid) + // revive:disable-next-line + logger.Fatal(). + Str("uid", cfg.uid). + Msg("keeper uid not valid. " + + "It can contain only lower-case letters, numbers and the underscore character") } } @@ -2101,28 +2385,30 @@ func keeper(c *cobra.Command, args []string) { end := make(chan error) sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go sigHandler(sigs, cancel) + go sigHandler(ctx, sigs, cancel) if cfg.MetricsListenAddress != "" { http.Handle("/metrics", promhttp.Handler()) go func() { err = http.ListenAndServe(cfg.MetricsListenAddress, nil) if err != nil { - log.Errorw("metrics http server error", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("metrics http server error") cancel() } }() } - p, err := NewPostgresKeeper(&cfg, end) + p, err := NewPostgresKeeper(ctx, &cfg, end) if err != nil { - log.Fatalf("cannot create keeper: %v", err) + logger.Fatal().AnErr("err", err).Msg("cannot create keeper") } go p.Start(ctx) <-end if !cfg.disableDataDirLocking { - lockFile.Close() + if err := lockFile.Close(); err != nil { + logger.Fatal().AnErr("err", err).Msg("closing lock file failed") + } } } diff --git a/cmd/keeper/cmd/keeper_test.go b/cmd/keeper/cmd/keeper_test.go index 8d2b0f584..ec69f4f29 100644 --- a/cmd/keeper/cmd/keeper_test.go +++ b/cmd/keeper/cmd/keeper_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +17,18 @@ package cmd import ( "bytes" + "context" "errors" "fmt" "reflect" "testing" "github.com/golang/mock/gomock" - pgmocks "github.com/sorintlab/stolon/internal/mock/postgresql" - pg "github.com/sorintlab/stolon/internal/postgresql" + pgmocks "github.com/pgvillage-tools/stolon/internal/mock/postgresql" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" ) func TestParseSynchronousStandbyNames(t *testing.T) { @@ -82,43 +84,51 @@ func TestParseSynchronousStandbyNames(t *testing.T) { } func TestGenerateHBA(t *testing.T) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + const ( + db1 = "db1" + db2 = "db2" + db3 = "db3" + ) + // minimal clusterdata with only the fields used by generateHBA - cd := &cluster.ClusterData{ + cd := &cluster.Data{ Cluster: &cluster.Cluster{ - Spec: &cluster.ClusterSpec{}, + Spec: &cluster.Spec{}, Status: cluster.ClusterStatus{}, }, Keepers: cluster.Keepers{}, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", + db1: &cluster.DB{ + UID: db1, Spec: &cluster.DBSpec{ - Role: common.RoleMaster, + Role: common.RolePrimary, }, Status: cluster.DBStatus{ ListenAddress: "192.168.0.1", }, }, - "db2": &cluster.DB{ - UID: "db2", + db2: &cluster.DB{ + UID: db2, Spec: &cluster.DBSpec{ - Role: common.RoleStandby, + Role: common.RoleReplica, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: db1, }, }, Status: cluster.DBStatus{ ListenAddress: "192.168.0.2", }, }, - "db3": &cluster.DB{ - UID: "db3", + db3: &cluster.DB{ + UID: db3, Spec: &cluster.DBSpec{ - Role: common.RoleStandby, + Role: common.RoleReplica, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: db1, }, }, Status: cluster.DBStatus{ @@ -129,6 +139,10 @@ func TestGenerateHBA(t *testing.T) { Proxy: &cluster.Proxy{}, } + const ( + defaultLine1 = "local postgres superuser md5" + defaultLine2 = "local replication repluser md5" + ) tests := []struct { DefaultSUReplAccessMode cluster.SUReplAccessMode dbUID string @@ -137,10 +151,10 @@ func TestGenerateHBA(t *testing.T) { }{ { DefaultSUReplAccessMode: cluster.SUReplAccessAll, - dbUID: "db1", + dbUID: db1, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all superuser 0.0.0.0/0 md5", "host all superuser ::0/0 md5", "host replication repluser 0.0.0.0/0 md5", @@ -151,10 +165,10 @@ func TestGenerateHBA(t *testing.T) { }, { DefaultSUReplAccessMode: cluster.SUReplAccessAll, - dbUID: "db2", + dbUID: db2, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all superuser 0.0.0.0/0 md5", "host all superuser ::0/0 md5", "host replication repluser 0.0.0.0/0 md5", @@ -165,13 +179,13 @@ func TestGenerateHBA(t *testing.T) { }, { DefaultSUReplAccessMode: cluster.SUReplAccessAll, - dbUID: "db1", + dbUID: db1, pgHBA: []string{ "host all all 192.168.0.0/24 md5", }, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all superuser 0.0.0.0/0 md5", "host all superuser ::0/0 md5", "host replication repluser 0.0.0.0/0 md5", @@ -181,13 +195,13 @@ func TestGenerateHBA(t *testing.T) { }, { DefaultSUReplAccessMode: cluster.SUReplAccessAll, - dbUID: "db2", + dbUID: db2, pgHBA: []string{ "host all all 192.168.0.0/24 md5", }, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all superuser 0.0.0.0/0 md5", "host all superuser ::0/0 md5", "host replication repluser 0.0.0.0/0 md5", @@ -197,10 +211,10 @@ func TestGenerateHBA(t *testing.T) { }, { DefaultSUReplAccessMode: cluster.SUReplAccessStrict, - dbUID: "db1", + dbUID: db1, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all superuser 192.168.0.2/32 md5", "host replication repluser 192.168.0.2/32 md5", "host all superuser 192.168.0.3/32 md5", @@ -211,10 +225,10 @@ func TestGenerateHBA(t *testing.T) { }, { DefaultSUReplAccessMode: cluster.SUReplAccessStrict, - dbUID: "db2", + dbUID: db2, out: []string{ - "local postgres superuser md5", - "local replication repluser md5", + defaultLine1, + defaultLine2, "host all all 0.0.0.0/0 md5", "host all all ::0/0 md5", }, @@ -223,10 +237,14 @@ func TestGenerateHBA(t *testing.T) { for i, tt := range tests { p := &PostgresKeeper{ - pgSUAuthMethod: "md5", - pgSUUsername: "superuser", - pgReplAuthMethod: "md5", - pgReplUsername: "repluser", + pgSUConnType: "host", + pgSULocalAuthMethod: "md5", + pgSUAuthMethod: "md5", + pgSUUsername: "superuser", + pgReplConnType: "host", + pgReplLocalAuthMethod: "md5", + pgReplAuthMethod: "md5", + pgReplUsername: "repluser", } cd.Cluster.Spec.DefaultSUReplAccessMode = &tt.DefaultSUReplAccessMode @@ -234,7 +252,7 @@ func TestGenerateHBA(t *testing.T) { db := cd.DBs[tt.dbUID] db.Spec.PGHBA = tt.pgHBA - out := p.generateHBA(cd, db, false) + out := p.generateHBA(ctx, cd, db, false) if !reflect.DeepEqual(out, tt.out) { var b bytes.Buffer @@ -242,22 +260,24 @@ func TestGenerateHBA(t *testing.T) { for _, o := range out { b.WriteString(fmt.Sprintf("%s\n", o)) } - b.WriteString(fmt.Sprintf("\nwant:\n")) + b.WriteString("\nwant:\n") for _, o := range tt.out { b.WriteString(fmt.Sprintf("%s\n", o)) } - t.Errorf(b.String()) + t.Error(b.String()) } } } func TestGetTimeLinesHistory(t *testing.T) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() t.Run("should return empty if timelineID is not greater than 1", func(t *testing.T) { pgState := &cluster.PostgresState{ TimelineID: 1, } - ctlsh, err := getTimeLinesHistory(pgState, nil, 3) + ctlsh, err := getTimeLinesHistory(ctx, pgState, nil, 3) if err != nil { t.Errorf("err should be nil, but got %v", err) @@ -277,8 +297,10 @@ func TestGetTimeLinesHistory(t *testing.T) { defer ctrl.Finish() pgm := pgmocks.NewMockPGManager(ctrl) - pgm.EXPECT().GetTimelinesHistory(timelineID).Return([]*pg.TimelineHistory{}, fmt.Errorf("failed to get timeline history")) - ctlsh, err := getTimeLinesHistory(pgState, pgm, 3) + pgm.EXPECT().GetTimelinesHistory(timelineID).Return( + []*pg.TimelineHistory{}, + errors.New("failed to get timeline history")) + ctlsh, err := getTimeLinesHistory(ctx, pgState, pgm, 3) if err == nil { t.Errorf("err should be not be nil") @@ -291,117 +313,122 @@ func TestGetTimeLinesHistory(t *testing.T) { } }) - t.Run("should return timeline history as is if the given length is less than maxPostgresTimelinesHistory", func(t *testing.T) { - var timelineID uint64 = 2 - pgState := &cluster.PostgresState{ - TimelineID: timelineID, - } + t.Run( + "should return timeline history as is if the given length is less than maxPostgresTimelinesHistory", + func(t *testing.T) { + var timelineID uint64 = 2 + pgState := &cluster.PostgresState{ + TimelineID: timelineID, + } - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - pgm := pgmocks.NewMockPGManager(ctrl) - timelineHistories := []*pg.TimelineHistory{ - { - TimelineID: 1, - SwitchPoint: 1, - Reason: "reason1", - }, - { - TimelineID: 2, - SwitchPoint: 2, - Reason: "reason2", - }, - } - pgm.EXPECT().GetTimelinesHistory(timelineID).Return(timelineHistories, nil) - ctlsh, err := getTimeLinesHistory(pgState, pgm, 3) + pgm := pgmocks.NewMockPGManager(ctrl) + timelineHistories := []*pg.TimelineHistory{ + { + TimelineID: 1, + SwitchPoint: 1, + Reason: "reason1", + }, + { + TimelineID: 2, + SwitchPoint: 2, + Reason: "reason2", + }, + } + pgm.EXPECT().GetTimelinesHistory(timelineID).Return(timelineHistories, nil) + ctlsh, err := getTimeLinesHistory(ctx, pgState, pgm, 3) - if err != nil { - t.Errorf("err should be not be nil") - } - if len(ctlsh) != 2 { - t.Errorf("expecting length of ctlsh to be 2, but got %d", len(ctlsh)) - } - expectedTimelineHistories := cluster.PostgresTimelinesHistory{ - &cluster.PostgresTimelineHistory{ - TimelineID: 1, - SwitchPoint: 1, - Reason: "reason1", - }, - &cluster.PostgresTimelineHistory{ - TimelineID: 2, - SwitchPoint: 2, - Reason: "reason2", - }, - } - fmt.Println(ctlsh, expectedTimelineHistories) - if *ctlsh[0] != *expectedTimelineHistories[0] { - t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[0], *ctlsh[0]) - } - if *ctlsh[1] != *expectedTimelineHistories[1] { - t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[1], *ctlsh[1]) - } - }) + if err != nil { + t.Errorf("err should be not be nil") + } + if len(ctlsh) != 2 { + t.Errorf("expecting length of ctlsh to be 2, but got %d", len(ctlsh)) + } + expectedTimelineHistories := cluster.PostgresTimelinesHistory{ + &cluster.PostgresTimelineHistory{ + TimelineID: 1, + SwitchPoint: 1, + Reason: "reason1", + }, + &cluster.PostgresTimelineHistory{ + TimelineID: 2, + SwitchPoint: 2, + Reason: "reason2", + }, + } + fmt.Println(ctlsh, expectedTimelineHistories) + if *ctlsh[0] != *expectedTimelineHistories[0] { + t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[0], *ctlsh[0]) + } + if *ctlsh[1] != *expectedTimelineHistories[1] { + t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[1], *ctlsh[1]) + } + }) - t.Run("should return timeline history with last maxPostgresTimelinesHistory elements if timeline history length is greater than maxPostgresTimelinesHistory", func(t *testing.T) { - var timelineID uint64 = 4 - pgState := &cluster.PostgresState{ - TimelineID: timelineID, - } + t.Run( + // revive:disable-next-line + "should return timeline history with last maxPostgresTimelinesHistory elements if timeline history length is greater than maxPostgresTimelinesHistory", + func(t *testing.T) { + var timelineID uint64 = 4 + pgState := &cluster.PostgresState{ + TimelineID: timelineID, + } - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - pgm := pgmocks.NewMockPGManager(ctrl) - timelineHistories := []*pg.TimelineHistory{ - { - TimelineID: 1, - SwitchPoint: 1, - Reason: "reason1", - }, - { - TimelineID: 2, - SwitchPoint: 2, - Reason: "reason2", - }, - { - TimelineID: 3, - SwitchPoint: 3, - Reason: "reason3", - }, - { - TimelineID: 4, - SwitchPoint: 4, - Reason: "reason4", - }, - } - pgm.EXPECT().GetTimelinesHistory(timelineID).Return(timelineHistories, nil) - ctlsh, err := getTimeLinesHistory(pgState, pgm, 2) + pgm := pgmocks.NewMockPGManager(ctrl) + timelineHistories := []*pg.TimelineHistory{ + { + TimelineID: 1, + SwitchPoint: 1, + Reason: "reason1", + }, + { + TimelineID: 2, + SwitchPoint: 2, + Reason: "reason2", + }, + { + TimelineID: 3, + SwitchPoint: 3, + Reason: "reason3", + }, + { + TimelineID: 4, + SwitchPoint: 4, + Reason: "reason4", + }, + } + pgm.EXPECT().GetTimelinesHistory(timelineID).Return(timelineHistories, nil) + ctlsh, err := getTimeLinesHistory(ctx, pgState, pgm, 2) - if err != nil { - t.Errorf("err should be not be nil") - } - if len(ctlsh) != 2 { - t.Errorf("expecting length of ctlsh to be 2, but got %d", len(ctlsh)) - } - expectedTimelineHistories := cluster.PostgresTimelinesHistory{ - &cluster.PostgresTimelineHistory{ - TimelineID: 3, - SwitchPoint: 3, - Reason: "reason3", - }, - &cluster.PostgresTimelineHistory{ - TimelineID: 4, - SwitchPoint: 4, - Reason: "reason4", - }, - } - fmt.Println(ctlsh, expectedTimelineHistories) - if *ctlsh[0] != *expectedTimelineHistories[0] { - t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[0], *ctlsh[0]) - } - if *ctlsh[1] != *expectedTimelineHistories[1] { - t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[1], *ctlsh[1]) - } - }) + if err != nil { + t.Errorf("err should be not be nil") + } + if len(ctlsh) != 2 { + t.Errorf("expecting length of ctlsh to be 2, but got %d", len(ctlsh)) + } + expectedTimelineHistories := cluster.PostgresTimelinesHistory{ + &cluster.PostgresTimelineHistory{ + TimelineID: 3, + SwitchPoint: 3, + Reason: "reason3", + }, + &cluster.PostgresTimelineHistory{ + TimelineID: 4, + SwitchPoint: 4, + Reason: "reason4", + }, + } + fmt.Println(ctlsh, expectedTimelineHistories) + if *ctlsh[0] != *expectedTimelineHistories[0] { + t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[0], *ctlsh[0]) + } + if *ctlsh[1] != *expectedTimelineHistories[1] { + t.Errorf("expected %v, but got %v ", *expectedTimelineHistories[1], *ctlsh[1]) + } + }) } diff --git a/cmd/keeper/cmd/metrics.go b/cmd/keeper/cmd/metrics.go index ee17a86ae..cbbada0eb 100644 --- a/cmd/keeper/cmd/metrics.go +++ b/cmd/keeper/cmd/metrics.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd holds all CLI code for the keeper package cmd import ( + "github.com/pgvillage-tools/stolon/internal/common" "github.com/prometheus/client_golang/prometheus" - "github.com/sorintlab/stolon/internal/common" ) var ( diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go index 68051ad11..e6f716c15 100644 --- a/cmd/keeper/main.go +++ b/cmd/keeper/main.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package main is a package that provides functionality concerning postgresSQL lifecycles package main import ( - "github.com/sorintlab/stolon/cmd/keeper/cmd" + "github.com/pgvillage-tools/stolon/cmd/keeper/cmd" ) func main() { diff --git a/cmd/proxy/cmd/proxy.go b/cmd/proxy/cmd/proxy.go index 39f1f3b99..45d3f692b 100644 --- a/cmd/proxy/cmd/proxy.go +++ b/cmd/proxy/cmd/proxy.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd holds all CLI code for the keeper package cmd import ( @@ -22,23 +24,21 @@ import ( "sync" "time" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/flagutil" + "github.com/pgvillage-tools/stolon/internal/logging" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/flagutil" - slog "github.com/sorintlab/stolon/internal/log" - "github.com/sorintlab/stolon/internal/store" - "github.com/sorintlab/stolon/internal/util" "github.com/davecgh/go-spew/spew" "github.com/sorintlab/pollon" "github.com/spf13/cobra" - "go.uber.org/zap" ) -var log = slog.S() - +// CmdProxy is the cobra command which defines running stolon-proxy var CmdProxy = &cobra.Command{ Use: "stolon-proxy", Run: proxy, @@ -63,19 +63,49 @@ var cfg config func init() { cmd.AddCommonFlags(CmdProxy, &cfg.CommonConfig) - CmdProxy.PersistentFlags().StringVar(&cfg.listenAddress, "listen-address", "127.0.0.1", "proxy listening address") - CmdProxy.PersistentFlags().StringVar(&cfg.port, "port", "5432", "proxy listening port") - CmdProxy.PersistentFlags().BoolVar(&cfg.stopListening, "stop-listening", true, "stop listening on store error") - CmdProxy.PersistentFlags().BoolVar(&cfg.debug, "debug", false, "enable debug logging") - CmdProxy.PersistentFlags().IntVar(&cfg.keepAliveIdle, "tcp-keepalive-idle", 0, "set tcp keepalive idle (seconds)") - CmdProxy.PersistentFlags().IntVar(&cfg.keepAliveCount, "tcp-keepalive-count", 0, "set tcp keepalive probe count number") - CmdProxy.PersistentFlags().IntVar(&cfg.keepAliveInterval, "tcp-keepalive-interval", 0, "set tcp keepalive interval (seconds)") + CmdProxy.PersistentFlags().StringVar( + &cfg.listenAddress, + "listen-address", + "127.0.0.1", + "proxy listening address") + CmdProxy.PersistentFlags().StringVar( + &cfg.port, + "port", + "5432", + "proxy listening port") + CmdProxy.PersistentFlags().BoolVar( + &cfg.stopListening, + "stop-listening", + true, + "stop listening on store error") + CmdProxy.PersistentFlags().BoolVar( + &cfg.debug, + "debug", + false, + "enable debug logging") + CmdProxy.PersistentFlags().IntVar( + &cfg.keepAliveIdle, + "tcp-keepalive-idle", + 0, + "set tcp keepalive idle (seconds)") + CmdProxy.PersistentFlags().IntVar( + &cfg.keepAliveCount, + "tcp-keepalive-count", + 0, + "set tcp keepalive probe count number") + CmdProxy.PersistentFlags().IntVar( + &cfg.keepAliveInterval, + "tcp-keepalive-interval", + 0, + "set tcp keepalive interval (seconds)") if err := CmdProxy.PersistentFlags().MarkDeprecated("debug", "use --log-level=debug instead"); err != nil { - log.Fatal(err) + _, logger := logging.GetLogComponent(context.Background(), logging.ProxyComponent) + logger.Fatal().AnErr("err", err).Msg("") } } +// ClusterChecker is struct containing information for checking the cluster type ClusterChecker struct { uid string listenAddress string @@ -95,8 +125,9 @@ type ClusterChecker struct { configMutex sync.Mutex } -func NewClusterChecker(uid string, cfg config) (*ClusterChecker, error) { - e, err := cmd.NewStore(&cfg.CommonConfig) +// NewClusterChecker is a function which creates a new clusterchecker +func NewClusterChecker(ctx context.Context, uid string, cfg config) (*ClusterChecker, error) { + e, err := cmd.NewStore(ctx, &cfg.CommonConfig) if err != nil { return nil, fmt.Errorf("cannot create store: %v", err) } @@ -114,14 +145,15 @@ func NewClusterChecker(uid string, cfg config) (*ClusterChecker, error) { }, nil } -func (c *ClusterChecker) startPollonProxy() error { +func (c *ClusterChecker) startPollonProxy(ctx context.Context) error { + _, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) c.pollonMutex.Lock() defer c.pollonMutex.Unlock() if c.pp != nil { return nil } - log.Infow("Starting proxying") + logger.Info().Msg("Starting proxying") addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(cfg.listenAddress, cfg.port)) if err != nil { return fmt.Errorf("error resolving tcp addr %q: %v", addr.String(), err) @@ -151,14 +183,17 @@ func (c *ClusterChecker) startPollonProxy() error { return nil } -func (c *ClusterChecker) stopPollonProxy() { +func (c *ClusterChecker) stopPollonProxy(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) c.pollonMutex.Lock() defer c.pollonMutex.Unlock() if c.pp != nil { - log.Infow("Stopping listening") + logger.Info().Msg("Stopping listening") c.pp.Stop() c.pp = nil - c.listener.Close() + if err := c.listener.Close(); err != nil { + logger.Fatal().AnErr("err", err).Msg("Stopping listening") + } c.listener = nil } } @@ -171,36 +206,37 @@ func (c *ClusterChecker) sendPollonConfData(confData pollon.ConfData) { } } -func (c *ClusterChecker) SetProxyInfo(e store.Store, generation int64, proxyTimeout time.Duration) error { +// SetProxyInfo is function which sets the proxy-info +func (c *ClusterChecker) SetProxyInfo(ctx context.Context, _ store.Store, generation int64, + proxyTimeout time.Duration) error { + _, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) proxyInfo := &cluster.ProxyInfo{ InfoUID: common.UID(), UID: c.uid, Generation: generation, ProxyTimeout: proxyTimeout, } - log.Debugf("proxyInfo dump: %s", spew.Sdump(proxyInfo)) + logger.Debug().Str("proxyInfo dump", spew.Sdump(proxyInfo)).Msg("") - if err := c.e.SetProxyInfo(context.TODO(), proxyInfo, 2*proxyTimeout); err != nil { - return err - } - return nil + return c.e.SetProxyInfo(ctx, proxyInfo, 2*proxyTimeout) } // Check reads the cluster data and applies the right pollon configuration. -func (c *ClusterChecker) Check() error { - cd, _, err := c.e.GetClusterData(context.TODO()) +func (c *ClusterChecker) Check(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) + cd, _, err := c.e.GetClusterData(ctx) if err != nil { return fmt.Errorf("cannot get cluster data: %v", err) } // Start pollon if not active - if err = c.startPollonProxy(); err != nil { + if err = c.startPollonProxy(ctx); err != nil { return fmt.Errorf("failed to start proxy: %v", err) } - log.Debugf("cd dump: %s", spew.Sdump(cd)) + logger.Debug().Str("cd dump", spew.Sdump(cd)).Msg("") if cd == nil { - log.Infow("no clusterdata available, closing connections to master") + logger.Info().Msg("no clusterdata available, closing connections to master") c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) return nil } @@ -217,7 +253,8 @@ func (c *ClusterChecker) Check() error { cdProxyTimeout := cd.Cluster.DefSpec().ProxyTimeout.Duration // use the greater between the current proxy timeout and the one defined in the cluster spec if they're different. - // in this way we're updating our proxyInfo using a timeout that is greater or equal the current active timeout timer. + // in this way we're updating our proxyInfo using a timeout that is greater + // or equal the current active timeout timer. c.configMutex.Lock() proxyTimeout := c.proxyTimeout if cdProxyTimeout > proxyTimeout { @@ -227,11 +264,11 @@ func (c *ClusterChecker) Check() error { proxy := cd.Proxy if proxy == nil { - log.Infow("no proxy object available, closing connections to master") + logger.Info().Msg("no proxy object available, closing connections to master") c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) // ignore errors on setting proxy info - if err = c.SetProxyInfo(c.e, cluster.NoGeneration, proxyTimeout); err != nil { - log.Errorw("failed to update proxyInfo", zap.Error(err)) + if err = c.SetProxyInfo(ctx, c.e, cluster.NoGeneration, proxyTimeout); err != nil { + logger.Error().AnErr("err", err).Msg("failed to update proxyInfo") } else { // update proxyCheckinterval and proxyTimeout only if we successfully updated our proxy info c.configMutex.Lock() @@ -244,11 +281,11 @@ func (c *ClusterChecker) Check() error { db, ok := cd.DBs[proxy.Spec.MasterDBUID] if !ok { - log.Infow("no db object available, closing connections to master", "db", proxy.Spec.MasterDBUID) + logger.Info().Str("db", proxy.Spec.MasterDBUID).Msg("no db object available, closing connections to master") c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) // ignore errors on setting proxy info - if err = c.SetProxyInfo(c.e, proxy.Generation, proxyTimeout); err != nil { - log.Errorw("failed to update proxyInfo", zap.Error(err)) + if err = c.SetProxyInfo(ctx, c.e, proxy.Generation, proxyTimeout); err != nil { + logger.Error().AnErr("err", err).Msg("failed to update proxyInfo") } else { // update proxyCheckinterval and proxyTimeout only if we successfully updated our proxy info c.configMutex.Lock() @@ -261,39 +298,42 @@ func (c *ClusterChecker) Check() error { addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(db.Status.ListenAddress, db.Status.Port)) if err != nil { - log.Errorw("cannot resolve db address", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("cannot resolve db address") c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) return nil } - log.Infow("master address", "address", addr) - if err = c.SetProxyInfo(c.e, proxy.Generation, proxyTimeout); err != nil { + logger.Info().Any("address", addr).Msg("master address") + if err = c.SetProxyInfo(ctx, c.e, proxy.Generation, proxyTimeout); err != nil { // if we failed to update our proxy info when a master is defined we // cannot ignore this error since the sentinel won't know that we exist // and are sending connections to a master so, when electing a new // master, it'll not wait for us to close connections to the old one. return fmt.Errorf("failed to update proxyInfo: %v", err) - } else { - // update proxyCheckinterval and proxyTimeout only if we successfully updated our proxy info - c.configMutex.Lock() - c.proxyCheckInterval = cdProxyCheckInterval - c.proxyTimeout = cdProxyTimeout - c.configMutex.Unlock() } + // update proxyCheckinterval and proxyTimeout only if we successfully updated our proxy info + c.configMutex.Lock() + c.proxyCheckInterval = cdProxyCheckInterval + c.proxyTimeout = cdProxyTimeout + c.configMutex.Unlock() // start proxing only if we are inside enabledProxies, this ensures that the // sentinel has read our proxyinfo and knows we are alive if util.StringInSlice(proxy.Spec.EnabledProxies, c.uid) { - log.Infow("proxying to master address", "address", addr) + logger.Info().Any("address", addr).Msg("proxying to master address") c.sendPollonConfData(pollon.ConfData{DestAddr: addr}) } else { - log.Infow("not proxying to master address since we aren't in the enabled proxies list", "address", addr) + logger.Info(). + Any("address", addr). + Msg("not proxying to master address since we aren't in the enabled proxies list") c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) } return nil } -func (c *ClusterChecker) TimeoutChecker(checkOkCh chan struct{}) { +// TimeoutChecker is a function that checks the timeouts +func (c *ClusterChecker) TimeoutChecker(ctx context.Context, checkOkCh chan struct{}) { + _, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) c.configMutex.Lock() timeoutTimer := time.NewTimer(c.proxyTimeout) c.configMutex.Unlock() @@ -301,17 +341,17 @@ func (c *ClusterChecker) TimeoutChecker(checkOkCh chan struct{}) { for { select { case <-timeoutTimer.C: - log.Infow("check timeout timer fired") + logger.Info().Msg("check timeout timer fired") // if the check timeouts close all connections and stop listening // (for example to avoid load balancers forward connections to us // since we aren't ready or in a bad state) c.sendPollonConfData(pollon.ConfData{DestAddr: nil}) if c.stopListening { - c.stopPollonProxy() + c.stopPollonProxy(ctx) } case <-checkOkCh: - log.Debugw("check ok message received") + logger.Debug().Msg("check ok message received") // ignore if stop succeeded or not due to timer already expired timeoutTimer.Stop() @@ -323,7 +363,9 @@ func (c *ClusterChecker) TimeoutChecker(checkOkCh chan struct{}) { } } -func (c *ClusterChecker) Start() error { +// Start is function that starts the procy +func (c *ClusterChecker) Start(ctx context.Context) error { + _, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) checkOkCh := make(chan struct{}) checkCh := make(chan error) timerCh := time.NewTimer(0).C @@ -332,18 +374,18 @@ func (c *ClusterChecker) Start() error { // if the Check method is blocked somewhere. // The idomatic/cleaner solution will be to use a context instead of this // TimeoutChecker but we have to change the libkv stores to support contexts. - go c.TimeoutChecker(checkOkCh) + go c.TimeoutChecker(ctx, checkOkCh) for { select { case <-timerCh: go func() { - checkCh <- c.Check() + checkCh <- c.Check(ctx) }() case err := <-checkCh: if err != nil { // don't report check ok since it returned an error - log.Infow("check function error", zap.Error(err)) + logger.Info().AnErr("err", err).Msg("check function error") } else { // report that check was ok checkOkCh <- struct{}{} @@ -360,79 +402,64 @@ func (c *ClusterChecker) Start() error { } } +// Execute is the main executor of the proxy func Execute() { + _, logger := logging.GetLogComponent(context.Background(), logging.ProxyComponent) if err := flagutil.SetFlagsFromEnv(CmdProxy.PersistentFlags(), "STPROXY"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } if err := CmdProxy.Execute(); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } } -func proxy(c *cobra.Command, args []string) { - switch cfg.LogLevel { - case "error": - slog.SetLevel(zap.ErrorLevel) - case "warn": - slog.SetLevel(zap.WarnLevel) - case "info": - slog.SetLevel(zap.InfoLevel) - case "debug": - slog.SetLevel(zap.DebugLevel) - default: - log.Fatalf("invalid log level: %v", cfg.LogLevel) - } +func proxy(c *cobra.Command, _ []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + ctx, logger := logging.GetLogComponent(ctx, logging.ProxyComponent) + defer cancelFunc() + logging.SetStaticLevel(cfg.LogLevel) if cfg.debug { - slog.SetDebug() + logging.SetStaticLevel("debug") } if cmd.IsColorLoggerEnable(c, &cfg.CommonConfig) { - log = slog.SColor() - } - if slog.IsDebug() { - if cmd.IsColorLoggerEnable(c, &cfg.CommonConfig) { - stdlog := slog.StdLogColor() - pollon.SetLogger(stdlog) - } else { - stdlog := slog.StdLog() - pollon.SetLogger(stdlog) - } + logging.EnableColor() } if err := cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { - log.Fatalf(err.Error()) + logger.Fatal().AnErr("err", err).Msg("") } cmd.SetMetrics(&cfg.CommonConfig, "proxy") if cfg.keepAliveIdle < 0 { - log.Fatalf("tcp keepalive idle value must be greater or equal to 0") + logger.Fatal().Msgf("tcp keepalive idle value must be greater or equal to 0") } if cfg.keepAliveCount < 0 { - log.Fatalf("tcp keepalive count value must be greater or equal to 0") + logger.Fatal().Msgf("tcp keepalive count value must be greater or equal to 0") } if cfg.keepAliveInterval < 0 { - log.Fatalf("tcp keepalive interval value must be greater or equal to 0") + logger.Fatal().Msgf("tcp keepalive interval value must be greater or equal to 0") } uid := common.UID() - log.Infow("proxy uid", "uid", uid) + logger.Info().Str("uid", uid).Msg("proxy uid") if cfg.MetricsListenAddress != "" { http.Handle("/metrics", promhttp.Handler()) go func() { err := http.ListenAndServe(cfg.MetricsListenAddress, nil) if err != nil { - log.Fatalf("metrics http server error", zap.Error(err)) + logger.Fatal().AnErr("err", err).Msg("metrics http server error") } }() } - clusterChecker, err := NewClusterChecker(uid, cfg) + clusterChecker, err := NewClusterChecker(ctx, uid, cfg) if err != nil { - log.Fatalf("cannot create cluster checker: %v", err) + logger.Fatal().Msgf("cannot create cluster checker: %v", err) } - if err = clusterChecker.Start(); err != nil { - log.Fatalf("cluster checker ended with error: %v", err) + if err = clusterChecker.Start(ctx); err != nil { + logger.Fatal().Msgf("cluster checker ended with error: %v", err) } } diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index e5cc4d590..d5a900b7f 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package main is a package that provides functionality concerning postgresSQL lifecycles package main import ( - "github.com/sorintlab/stolon/cmd/proxy/cmd" + "github.com/pgvillage-tools/stolon/cmd/proxy/cmd" ) func main() { diff --git a/cmd/sentinel/cmd/metrics.go b/cmd/sentinel/cmd/metrics.go index 8181d3315..a45c3ce43 100644 --- a/cmd/sentinel/cmd/metrics.go +++ b/cmd/sentinel/cmd/metrics.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd holds all CLI code for the keeper package cmd import ( diff --git a/cmd/sentinel/cmd/sentinel.go b/cmd/sentinel/cmd/sentinel.go index 512172e6d..b96ea9db5 100644 --- a/cmd/sentinel/cmd/sentinel.go +++ b/cmd/sentinel/cmd/sentinel.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,8 +18,8 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" - "io/ioutil" "math/rand" "net/http" "os" @@ -29,29 +30,43 @@ import ( "syscall" "time" + "github.com/Masterminds/semver/v3" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/flagutil" + "github.com/pgvillage-tools/stolon/internal/logging" + "github.com/pgvillage-tools/stolon/internal/postgresql" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/timer" + "github.com/pgvillage-tools/stolon/internal/util" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/flagutil" - slog "github.com/sorintlab/stolon/internal/log" - pg "github.com/sorintlab/stolon/internal/postgresql" - "github.com/sorintlab/stolon/internal/store" - "github.com/sorintlab/stolon/internal/timer" - "github.com/sorintlab/stolon/internal/util" "github.com/davecgh/go-spew/spew" "github.com/mitchellh/copystructure" "github.com/spf13/cobra" - "go.uber.org/zap" ) -var log = slog.S() - const ( fakeStandbyName = "stolonfakestandby" ) +const ( + logDB = "db" + logRecDB = "receivedDB" + logKeeper = "keeper" + logDbSysID = "dbSystemdID" + logMasterDB = "masterDB" + logMasterSystemID = "masterSystemID" + logPrevSyncStdbys = "prevSynchronousStandbys" + logSyncStdbys = "SynchronousStandbys" + logSyncStdbyDb = "synchronousStandbyDB" + logRequiredWal = "requiredWAL" + logOldMasterWal = "olderWAL" +) + +// CmdSentinel is a variable which contains the cobra command var CmdSentinel = &cobra.Command{ Use: "stolon-sentinel", Run: sentinel, @@ -67,31 +82,42 @@ type config struct { var cfg config func init() { + _, logger := logging.GetLogComponent(context.Background(), logging.SentinelComponent) cmd.AddCommonFlags(CmdSentinel, &cfg.CommonConfig) - CmdSentinel.PersistentFlags().StringVar(&cfg.initialClusterSpecFile, "initial-cluster-spec", "", "a file providing the initial cluster specification, used only at cluster initialization, ignored if cluster is already initialized") - CmdSentinel.PersistentFlags().BoolVar(&cfg.debug, "debug", false, "enable debug logging (deprecated, use log-level instead)") + CmdSentinel.PersistentFlags().StringVar( + &cfg.initialClusterSpecFile, + "initial-cluster-spec", + "", + // revive:disable-next-line + "a file providing the initial cluster specification, used only at cluster initialization, ignored if cluster is already initialized") + CmdSentinel.PersistentFlags().BoolVar( + &cfg.debug, + "debug", + false, + "enable debug logging (deprecated, use log-level instead)") if err := CmdSentinel.PersistentFlags().MarkDeprecated("debug", "use --log-level=debug instead"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } } func (s *Sentinel) electionLoop(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) for { - log.Infow("Trying to acquire sentinels leadership") - electedCh, errCh := s.election.RunForElection() + logger.Info().Msg("Trying to acquire sentinels leadership") + electedCh, errCh := s.election.RunForElection(ctx) for { select { case elected := <-electedCh: s.leaderMutex.Lock() if elected { - log.Infow("sentinel leadership acquired") + logger.Info().Msg("sentinel leadership acquired") s.leader = true s.leadershipCount++ } else { if s.leader { - log.Infow("sentinel leadership lost") + logger.Info().Msg("sentinel leadership lost") } s.leader = false } @@ -99,7 +125,7 @@ func (s *Sentinel) electionLoop(ctx context.Context) { case err := <-errCh: if err != nil { - log.Errorw("election loop error", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("election loop error") // It's important to Stop() any on-going elections, as most stores will block // until all previous elections have completed. If we continue without stopping, @@ -109,7 +135,7 @@ func (s *Sentinel) electionLoop(ctx context.Context) { } goto end case <-ctx.Done(): - log.Debugw("stopping election loop") + logger.Debug().Msg("stopping election loop") s.election.Stop() return } @@ -121,44 +147,47 @@ func (s *Sentinel) electionLoop(ctx context.Context) { // syncRepl return whether to use synchronous replication based on the current // cluster spec. -func (s *Sentinel) syncRepl(spec *cluster.ClusterSpec) bool { +func (s *Sentinel) syncRepl(spec *cluster.Spec) bool { // a cluster standby role means our "master" will act as a cascading standby to // the other keepers, in this case we can't use synchronous replication - return *spec.SynchronousReplication && *spec.Role == cluster.ClusterRoleMaster + return *spec.SynchronousReplication && *spec.Role == cluster.Primary } func (s *Sentinel) setSentinelInfo(ctx context.Context, ttl time.Duration) error { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) sentinelInfo := &cluster.SentinelInfo{ UID: s.uid, } - log.Debugw("sentinelInfo dump", "sentinelInfo", sentinelInfo) + logger.Debug().Any("sentinelInfo", sentinelInfo).Msg("sentinelInfo dump") - if err := s.e.SetSentinelInfo(ctx, sentinelInfo, ttl); err != nil { - return err - } - return nil + return s.e.SetSentinelInfo(ctx, sentinelInfo, ttl) } +// SetKeeperError is a function which handles keeper error timers func (s *Sentinel) SetKeeperError(uid string) { if _, ok := s.keeperErrorTimers[uid]; !ok { s.keeperErrorTimers[uid] = timer.Now() } } +// CleanKeeperError is a function which deletes keeper error timers and the uid func (s *Sentinel) CleanKeeperError(uid string) { delete(s.keeperErrorTimers, uid) } +// SetDBError is functiom that sets the timers for database errors func (s *Sentinel) SetDBError(uid string) { if _, ok := s.dbErrorTimers[uid]; !ok { s.dbErrorTimers[uid] = timer.Now() } } +// CleanDBError is function which cleans the database errors func (s *Sentinel) CleanDBError(uid string) { delete(s.dbErrorTimers, uid) } +// SetDBNotIncreasingXLogPos is a function that sets the map dbNotIncreasingXLogPos func (s *Sentinel) SetDBNotIncreasingXLogPos(uid string) { if _, ok := s.dbNotIncreasingXLogPos[uid]; !ok { s.dbNotIncreasingXLogPos[uid] = 1 @@ -167,11 +196,17 @@ func (s *Sentinel) SetDBNotIncreasingXLogPos(uid string) { } } +// CleanDBNotIncreasingXLogPos is function that cleanes the map dbNotIncreasingXLogPos func (s *Sentinel) CleanDBNotIncreasingXLogPos(uid string) { delete(s.dbNotIncreasingXLogPos, uid) } -func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo cluster.KeepersInfo, firstRun bool) (*cluster.ClusterData, KeeperInfoHistories) { +func (s *Sentinel) updateKeepersStatus( + ctx context.Context, + cd *cluster.Data, + keepersInfo cluster.KeepersInfo, + firstRun bool) (*cluster.Data, KeeperInfoHistories) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) // Create a copy of cd cd = cd.DeepCopy() @@ -181,10 +216,10 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus tmpKeepersInfo := keepersInfo.DeepCopy() for _, ki := range keepersInfo { if ki.ClusterUID != cd.Cluster.UID { - delete(tmpKeepersInfo, ki.UID) + delete(*tmpKeepersInfo, ki.UID) } } - keepersInfo = tmpKeepersInfo + keepersInfo = *tmpKeepersInfo // On first run just insert keepers info in the history with Seen set // to false and don't do any change to the keepers' state @@ -201,11 +236,11 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus if kih, ok := kihs[keeperUID]; ok { if kih.KeeperInfo.InfoUID == ki.InfoUID { if !kih.Seen { - //Remove since it was already there and wasn't updated - delete(tmpKeepersInfo, ki.UID) + // Remove since it was already there and wasn't updated + delete(*tmpKeepersInfo, ki.UID) } else if kih.Seen && timer.Since(kih.Timer) > s.sleepInterval { - //Remove since it wasn't updated - delete(tmpKeepersInfo, ki.UID) + // Remove since it wasn't updated + delete(*tmpKeepersInfo, ki.UID) } } if kih.KeeperInfo.InfoUID != ki.InfoUID { @@ -215,7 +250,7 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus kihs[keeperUID] = &KeeperInfoHistory{KeeperInfo: ki, Seen: true, Timer: timer.Now()} } } - keepersInfo = tmpKeepersInfo + keepersInfo = *tmpKeepersInfo // Create new keepers from keepersInfo for keeperUID, ki := range keepersInfo { @@ -273,29 +308,42 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus // Mark not found DBs in DBstates in error k, ok := keepersInfo[db.Spec.KeeperUID] if !ok { - log.Warnw("no keeper info available", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Warn(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg( + "no keeper info available") s.SetDBError(db.UID) continue } dbs := k.PostgresState if dbs == nil { - log.Warnw("no db state available", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Warn(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg( + "no db state available") s.SetDBError(db.UID) continue } if dbs.UID != db.UID { - log.Warnw("received db state for unexpected db uid", "receivedDB", dbs.UID, "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Warn(). + Str(logRecDB, dbs.UID). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("received db state for unexpected db uid") s.SetDBError(db.UID) continue } - log.Debugw("received db state", "db", db.UID, "keeper", db.Spec.KeeperUID) - + logger.Debug(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("received db state") if db.Status.XLogPos == dbs.XLogPos { s.SetDBNotIncreasingXLogPos(db.UID) } else { s.CleanDBNotIncreasingXLogPos(db.UID) } - db.Status.ListenAddress = dbs.ListenAddress db.Status.Port = dbs.Port db.Status.CurrentGeneration = dbs.Generation @@ -313,9 +361,7 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus } else { s.SetDBError(db.UID) } - } - // Update dbs' healthy state for _, db := range cd.DBs { db.Status.Healthy = s.isDBHealthy(cd, db) @@ -325,7 +371,6 @@ func (s *Sentinel) updateKeepersStatus(cd *cluster.ClusterData, keepersInfo clus db.Status.Healthy = false } } - return cd, kihs } @@ -344,7 +389,6 @@ func (s *Sentinel) activeProxiesInfos(proxiesInfo cluster.ProxiesInfo) cluster.P if _, ok := proxiesInfo[proxyUID]; !ok { delete(pihs, proxyUID) } - } activeProxiesInfo := proxiesInfo.DeepCopy() @@ -353,7 +397,7 @@ func (s *Sentinel) activeProxiesInfos(proxiesInfo cluster.ProxiesInfo) cluster.P if pih, ok := pihs[pi.UID]; ok { if pih.ProxyInfo.InfoUID == pi.InfoUID { if timer.Since(pih.Timer) > 2*pi.ProxyTimeout { - delete(activeProxiesInfo, pi.UID) + delete(*activeProxiesInfo, pi.UID) } } else { pihs[pi.UID] = &ProxyInfoHistory{ProxyInfo: pi, Timer: timer.Now()} @@ -366,12 +410,12 @@ func (s *Sentinel) activeProxiesInfos(proxiesInfo cluster.ProxiesInfo) cluster.P s.proxyInfoHistories = pihs - return activeProxiesInfo + return *activeProxiesInfo } -func (s *Sentinel) findInitialKeeper(cd *cluster.ClusterData) (*cluster.Keeper, error) { +func (s *Sentinel) findInitialKeeper(cd *cluster.Data) (*cluster.Keeper, error) { if len(cd.Keepers) < 1 { - return nil, fmt.Errorf("no keepers registered") + return nil, errors.New("no keepers registered") } r := s.RandFn(len(cd.Keepers)) keys := []string{} @@ -383,7 +427,7 @@ func (s *Sentinel) findInitialKeeper(cd *cluster.ClusterData) (*cluster.Keeper, } // setDBSpecFromClusterSpec updates dbSpec values with the related clusterSpec ones -func (s *Sentinel) setDBSpecFromClusterSpec(cd *cluster.ClusterData) { +func (s *Sentinel) setDBSpecFromClusterSpec(cd *cluster.Data) { clusterSpec := cd.Cluster.DefSpec() for _, db := range cd.DBs { db.Spec.RequestTimeout = *clusterSpec.RequestTimeout @@ -407,9 +451,13 @@ func (s *Sentinel) setDBSpecFromClusterSpec(cd *cluster.ClusterData) { } } -func (s *Sentinel) isDifferentTimelineBranch(followedDB *cluster.DB, db *cluster.DB) bool { +func (s *Sentinel) isDifferentTimelineBranch(ctx context.Context, followedDB *cluster.DB, db *cluster.DB) bool { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) if followedDB.Status.TimelineID < db.Status.TimelineID { - log.Infow("followed instance timeline < than our timeline", "followedTimeline", followedDB.Status.TimelineID, "timeline", db.Status.TimelineID) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("timeline", db.Status.TimelineID). + Msg("followed instance timeline < than our timeline") return true } @@ -428,7 +476,12 @@ func (s *Sentinel) isDifferentTimelineBranch(followedDB *cluster.DB, db *cluster if ftlh.SwitchPoint == tlh.SwitchPoint { return false } - log.Infow("followed instance timeline forked at a different xlog pos than our timeline", "followedTimeline", followedDB.Status.TimelineID, "followedXlogpos", ftlh.SwitchPoint, "timeline", db.Status.TimelineID, "xlogpos", tlh.SwitchPoint) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("followedXlogpos", ftlh.SwitchPoint). + Uint64("timeline", db.Status.TimelineID). + Uint64("xlogpos", tlh.SwitchPoint). + Msg("followed instance timeline forked at a different xlog pos than our timeline") return true } @@ -436,7 +489,12 @@ func (s *Sentinel) isDifferentTimelineBranch(followedDB *cluster.DB, db *cluster ftlh := followedDB.Status.TimelinesHistory.GetTimelineHistory(db.Status.TimelineID) if ftlh != nil { if ftlh.SwitchPoint < db.Status.XLogPos { - log.Infow("followed instance timeline forked before our current state", "followedTimeline", followedDB.Status.TimelineID, "followedXlogpos", ftlh.SwitchPoint, "timeline", db.Status.TimelineID, "xlogpos", db.Status.XLogPos) + logger.Info(). + Uint64("followedTimeline", followedDB.Status.TimelineID). + Uint64("followedXlogpos", ftlh.SwitchPoint). + Uint64("timeline", db.Status.TimelineID). + Uint64("xlogpos", db.Status.XLogPos). + Msg("followed instance timeline forked before our current state") return true } } @@ -445,16 +503,25 @@ func (s *Sentinel) isDifferentTimelineBranch(followedDB *cluster.DB, db *cluster // isLagBelowMax checks if the db reported lag is below MaxStandbyLag from the // master reported lag -func (s *Sentinel) isLagBelowMax(cd *cluster.ClusterData, curMasterDB, db *cluster.DB) bool { - log.Debugf("curMasterDB.Status.XLogPos: %d, db.Status.XLogPos: %d, lag: %d", curMasterDB.Status.XLogPos, db.Status.XLogPos, int64(curMasterDB.Status.XLogPos-db.Status.XLogPos)) +func (s *Sentinel) isLagBelowMax(ctx context.Context, cd *cluster.Data, curMasterDB, db *cluster.DB) bool { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) + logger.Debug(). + Uint64("curMasterDB.Status.XLogPos", curMasterDB.Status.XLogPos). + Uint64("db.STatus.XLogPod", db.Status.XLogPos). + Uint64("delta", curMasterDB.Status.XLogPos-db.Status.XLogPos). + Msg("") if int64(curMasterDB.Status.XLogPos-db.Status.XLogPos) > int64(*cd.Cluster.DefSpec().MaxStandbyLag) { - log.Infow("ignoring keeper since its behind that maximum xlog position", "db", db.UID, "dbXLogPos", db.Status.XLogPos, "masterXLogPos", curMasterDB.Status.XLogPos) + logger.Info(). + Str(logDB, db.UID). + Uint64("dbXLogPos", db.Status.XLogPos). + Uint64("masterXLogPos", curMasterDB.Status.XLogPos). + Msg("ignoring keeper since its behind that maximum xlog position") return false } return true } -func (s *Sentinel) freeKeepers(cd *cluster.ClusterData) []*cluster.Keeper { +func (s *Sentinel) freeKeepers(cd *cluster.Data) []*cluster.Keeper { freeKeepers := []*cluster.Keeper{} K: for _, keeper := range cd.Keepers { @@ -495,15 +562,15 @@ const ( // * Has a master db role or a standby db role with followtype external // A standby is a db that: // * Has a standby db role with followtype internal -func (s *Sentinel) dbType(cd *cluster.ClusterData, dbUID string) dbType { +func (s *Sentinel) dbType(cd *cluster.Data, dbUID string) dbType { db, ok := cd.DBs[dbUID] if !ok { panic(fmt.Errorf("requested unexisting db uid %q", dbUID)) } switch db.Spec.Role { - case common.RoleMaster: + case common.RolePrimary: return dbTypeMaster - case common.RoleStandby: + case common.RoleReplica: if db.Spec.FollowConfig.Type == cluster.FollowTypeExternal { return dbTypeMaster } @@ -518,7 +585,8 @@ func (s *Sentinel) dbType(cd *cluster.ClusterData, dbUID string) dbType { // different timeline branch // dbs with CurrentGeneration == NoGeneration (0) are reported as // dbValidityUnknown since the db status is empty. -func (s *Sentinel) dbValidity(cd *cluster.ClusterData, dbUID string) dbValidity { +func (s *Sentinel) dbValidity(ctx context.Context, cd *cluster.Data, dbUID string) dbValidity { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) db, ok := cd.DBs[dbUID] if !ok { panic(fmt.Errorf("requested unexisting db uid %q", dbUID)) @@ -534,13 +602,18 @@ func (s *Sentinel) dbValidity(cd *cluster.ClusterData, dbUID string) dbValidity if db.Status.SystemID != "" { // if with a different postgres systemID it's invalid if db.Status.SystemID != masterDB.Status.SystemID { - log.Infow("invalid db since the postgres systemdID is different that the master one", "db", db.UID, "keeper", db.Spec.KeeperUID, "dbSystemdID", db.Status.SystemID, "masterSystemID", masterDB.Status.SystemID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Str(logDbSysID, db.Status.SystemID). + Str(logMasterSystemID, masterDB.Status.SystemID). + Msg("invalid db since the postgres systemdID is different that the master one") return dbValidityInvalid } } // If on a different timeline branch it's invalid - if s.isDifferentTimelineBranch(masterDB, db) { + if s.isDifferentTimelineBranch(ctx, masterDB, db) { return dbValidityInvalid } @@ -548,7 +621,8 @@ func (s *Sentinel) dbValidity(cd *cluster.ClusterData, dbUID string) dbValidity return dbValidityValid } -func (s *Sentinel) dbCanSync(cd *cluster.ClusterData, dbUID string) bool { +func (s *Sentinel) dbCanSync(ctx context.Context, cd *cluster.Data, dbUID string) bool { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) db, ok := cd.DBs[dbUID] if !ok { panic(fmt.Errorf("requested unexisting db uid %q", dbUID)) @@ -587,7 +661,7 @@ func (s *Sentinel) dbCanSync(cd *cluster.ClusterData, dbUID string) bool { // check only if the xlogpos isn't increasing for some time. This can also // happen when no writes are happening on the master but the standby should // be, if syncing at the same xlogpos. - if s.isDBIncreasingXLogPos(cd, db) { + if s.isDBIncreasingXLogPos(db) { return true } @@ -595,19 +669,31 @@ func (s *Sentinel) dbCanSync(cd *cluster.ClusterData, dbUID string) bool { older, err := pg.WalFileNameNoTimeLine(masterDB.Status.OlderWalFile) if err != nil { // warn on wrong file name (shouldn't happen...) - log.Warnw("wrong wal file name", "filename", masterDB.Status.OlderWalFile) - } - log.Debugw("xlog pos isn't advancing on standby, checking if the master has the required wals", "db", db.UID, "keeper", db.Spec.KeeperUID, "requiredWAL", required, "olderMasterWAL", older) + logger.Warn(). + Str("filename", masterDB.Status.OlderWalFile). + Msg("wrong wal file name") + } + logger.Debug(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Str(logRequiredWal, required). + Str(logOldMasterWal, older). + Msg("xlog pos isn't advancing on standby, checking if the master has the required wals") // compare the required wal file with the older wal file name ignoring the timelineID if required >= older { return true } - log.Infow("db won't be able to sync due to missing required wals on master", "db", db.UID, "keeper", db.Spec.KeeperUID, "requiredWAL", required, "olderMasterWAL", older) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Str(logRequiredWal, required). + Str(logOldMasterWal, older). + Msg("db won't be able to sync due to missing required wals on master") return false } -func (s *Sentinel) dbStatus(cd *cluster.ClusterData, dbUID string) dbStatus { +func (s *Sentinel) dbStatus(cd *cluster.Data, dbUID string) dbStatus { db, ok := cd.DBs[dbUID] if !ok { panic(fmt.Errorf("requested unexisting db uid %q", dbUID)) @@ -622,10 +708,9 @@ func (s *Sentinel) dbStatus(cd *cluster.ClusterData, dbUID string) dbStatus { convergenceTimeout := cd.Cluster.DefSpec().ConvergenceTimeout.Duration // check if db should be in init mode and adjust convergence timeout if db.Generation == cluster.InitialGeneration { - if db.Spec.InitMode == cluster.DBInitModeResync { + if db.Spec.InitMode == cluster.ResyncDB { convergenceTimeout = cd.Cluster.DefSpec().SyncTimeout.Duration } - } convergenceState := s.dbConvergenceState(db, convergenceTimeout) switch convergenceState { @@ -653,14 +738,18 @@ func (s *Sentinel) dbStatus(cd *cluster.ClusterData, dbUID string) dbStatus { return dbStatusGood } -func (s *Sentinel) validMastersByStatus(cd *cluster.ClusterData) (map[string]*cluster.DB, map[string]*cluster.DB, map[string]*cluster.DB) { +func (s *Sentinel) validMastersByStatus(ctx context.Context, cd *cluster.Data) ( + map[string]*cluster.DB, + map[string]*cluster.DB, + map[string]*cluster.DB, +) { goodMasters := map[string]*cluster.DB{} failedMasters := map[string]*cluster.DB{} convergingMasters := map[string]*cluster.DB{} for _, db := range cd.DBs { // keep only valid masters - if s.dbValidity(cd, db.UID) != dbValidityValid || s.dbType(cd, db.UID) != dbTypeMaster { + if s.dbValidity(ctx, cd, db.UID) != dbValidityValid || s.dbType(cd, db.UID) != dbTypeMaster { continue } status := s.dbStatus(cd, db.UID) @@ -676,14 +765,18 @@ func (s *Sentinel) validMastersByStatus(cd *cluster.ClusterData) (map[string]*cl return goodMasters, failedMasters, convergingMasters } -func (s *Sentinel) validStandbysByStatus(cd *cluster.ClusterData) (map[string]*cluster.DB, map[string]*cluster.DB, map[string]*cluster.DB) { +func (s *Sentinel) validStandbysByStatus(ctx context.Context, cd *cluster.Data) ( + map[string]*cluster.DB, + map[string]*cluster.DB, + map[string]*cluster.DB, +) { goodStandbys := map[string]*cluster.DB{} failedStandbys := map[string]*cluster.DB{} convergingStandbys := map[string]*cluster.DB{} for _, db := range cd.DBs { // keep only valid standbys - if s.dbValidity(cd, db.UID) != dbValidityValid || s.dbType(cd, db.UID) != dbTypeStandby { + if s.dbValidity(ctx, cd, db.UID) != dbValidityValid || s.dbType(cd, db.UID) != dbTypeStandby { continue } status := s.dbStatus(cd, db.UID) @@ -706,20 +799,29 @@ func (p dbSlice) Len() int { return len(p) } func (p dbSlice) Less(i, j int) bool { return p[i].Status.XLogPos < p[j].Status.XLogPos } func (p dbSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (s *Sentinel) findBestStandbys(cd *cluster.ClusterData, masterDB *cluster.DB) []*cluster.DB { - goodStandbys, _, _ := s.validStandbysByStatus(cd) +func (s *Sentinel) findBestStandbys(ctx context.Context, cd *cluster.Data, masterDB *cluster.DB) []*cluster.DB { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) + goodStandbys, _, _ := s.validStandbysByStatus(ctx, cd) bestDBs := []*cluster.DB{} for _, db := range goodStandbys { if db.Status.TimelineID != masterDB.Status.TimelineID { - log.Debugw("ignoring keeper since its pg timeline is different than master timeline", "db", db.UID, "dbTimeline", db.Status.TimelineID, "masterTimeline", masterDB.Status.TimelineID) + logger.Debug(). + Str(logDB, db.UID). + Uint64("dbTimeline", db.Status.TimelineID). + Uint64("masterTimeline", masterDB.Status.TimelineID). + Msg("ignoring keeper since its pg timeline is different than master timeline") continue } // do this only when not using synchronous replication since in sync repl we // have to ignore the last reported xlogpos or valid sync standby will be // skipped if !s.syncRepl(cd.Cluster.DefSpec()) { - if !s.isLagBelowMax(cd, masterDB, db) { - log.Debugw("ignoring keeper since its lag is above the max configured lag", "db", db.UID, "dbXLogPos", db.Status.XLogPos, "masterXLogPos", masterDB.Status.XLogPos) + if !s.isLagBelowMax(ctx, cd, masterDB, db) { + logger.Debug(). + Str(logDB, db.UID). + Uint64("dbXLogPos", db.Status.XLogPos). + Uint64("masterXLogPos", masterDB.Status.XLogPos). + Msg("ignoring keeper since its lag is above the max configured lag") continue } } @@ -734,11 +836,15 @@ func (s *Sentinel) findBestStandbys(cd *cluster.ClusterData, masterDB *cluster.D // this by selecting from valid standbys (those keepers that follow the same timeline as // our master, and have an acceptable replication lag) and also selecting from those nodes // that are valid to become master by their status. -func (s *Sentinel) findBestNewMasters(cd *cluster.ClusterData, masterDB *cluster.DB) []*cluster.DB { +func (s *Sentinel) findBestNewMasters(ctx context.Context, cd *cluster.Data, masterDB *cluster.DB) []*cluster.DB { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) bestNewMasters := []*cluster.DB{} - for _, db := range s.findBestStandbys(cd, masterDB) { + for _, db := range s.findBestStandbys(ctx, cd, masterDB) { if k, ok := cd.Keepers[db.Spec.KeeperUID]; ok && (k.Status.CanBeMaster != nil && !*k.Status.CanBeMaster) { - log.Infow("ignoring keeper since it cannot be master (--can-be-master=false)", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("ignoring keeper since it cannot be master (--can-be-master=false)") continue } @@ -746,16 +852,23 @@ func (s *Sentinel) findBestNewMasters(cd *cluster.ClusterData, masterDB *cluster } // Add the previous masters to the best standbys (if valid and in good state) - validMastersByStatus, _, _ := s.validMastersByStatus(cd) - log.Debugf("validMastersByStatus: %s", spew.Sdump(validMastersByStatus)) + validMastersByStatus, _, _ := s.validMastersByStatus(ctx, cd) + logger.Debug().Any("validMastersByStatus", spew.Sdump(validMastersByStatus)).Msg("validMastersByStatus") for _, db := range validMastersByStatus { if db.UID == masterDB.UID { - log.Debugw("ignoring db since it's the current master", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Debug(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("ignoring db since it's the current master") continue } if db.Status.TimelineID != masterDB.Status.TimelineID { - log.Debugw("ignoring keeper since its pg timeline is different than master timeline", "db", db.UID, "dbTimeline", db.Status.TimelineID, "masterTimeline", masterDB.Status.TimelineID) + logger.Debug(). + Str(logDB, db.UID). + Uint64("dbTimeline", db.Status.TimelineID). + Uint64("masterTimeline", masterDB.Status.TimelineID). + Msg("ignoring keeper since its pg timeline is different than master timeline") continue } @@ -763,8 +876,12 @@ func (s *Sentinel) findBestNewMasters(cd *cluster.ClusterData, masterDB *cluster // have to ignore the last reported xlogpos or valid sync standby will be // skipped if !s.syncRepl(cd.Cluster.DefSpec()) { - if !s.isLagBelowMax(cd, masterDB, db) { - log.Debugw("ignoring keeper since its lag is above the max configured lag", "db", db.UID, "dbXLogPos", db.Status.XLogPos, "masterXLogPos", masterDB.Status.XLogPos) + if !s.isLagBelowMax(ctx, cd, masterDB, db) { + logger.Debug(). + Str(logDB, db.UID). + Uint64("dbXLogPos", db.Status.XLogPos). + Uint64("masterXLogPos", masterDB.Status.XLogPos). + Msg("ignoring keeper since its lag is above the max configured lag") continue } } @@ -774,43 +891,47 @@ func (s *Sentinel) findBestNewMasters(cd *cluster.ClusterData, masterDB *cluster // Sort by XLogPos sort.Sort(dbSlice(bestNewMasters)) - log.Debugf("bestNewMasters: %s", spew.Sdump(bestNewMasters)) + logger.Debug().Any("bestNewMasters", spew.Sdump(bestNewMasters)).Msg("bestNewMasters") return bestNewMasters } -func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInfo) (*cluster.ClusterData, error) { +func (s *Sentinel) updateCluster(ctx context.Context, cd *cluster.Data, + pis cluster.ProxiesInfo) (*cluster.Data, error) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) // take a cd deepCopy to check that the code isn't changing it (it'll be a bug) origcd := cd.DeepCopy() newcd := cd.DeepCopy() clusterSpec := cd.Cluster.DefSpec() switch cd.Cluster.Status.Phase { - case cluster.ClusterPhaseInitializing: + case cluster.Initializing: switch *clusterSpec.InitMode { - case cluster.ClusterInitModeNew: + case cluster.New: // Is there already a keeper choosed to be the new master? if cd.Cluster.Status.Master == "" { - log.Infow("trying to find initial master") + logger.Info().Msg("trying to find initial master") k, err := s.findInitialKeeper(newcd) if err != nil { return nil, fmt.Errorf("cannot choose initial master: %v", err) } - log.Infow("initializing cluster", "keeper", k.UID) + logger.Info(). + Str(logKeeper, k.UID). + Msg("initializing cluster") db := &cluster.DB{ UID: s.UIDFn(), Generation: cluster.InitialGeneration, Spec: &cluster.DBSpec{ KeeperUID: k.UID, - InitMode: cluster.DBInitModeNew, + InitMode: cluster.NewDB, NewConfig: clusterSpec.NewConfig, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, IncludeConfig: *clusterSpec.MergePgParameters, }, } newcd.DBs[db.UID] = db newcd.Cluster.Status.Master = db.UID - log.Debugf("newcd dump: %s", spew.Sdump(newcd)) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump") } else { db, ok := newcd.DBs[cd.Cluster.Status.Master] if !ok { @@ -820,9 +941,12 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf switch s.dbConvergenceState(db, clusterSpec.InitTimeout.Duration) { case Converged: if db.Status.Healthy { - log.Infow("db initialized", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("db initialized") // Set db initMode to none, not needed but just a security measure - db.Spec.InitMode = cluster.DBInitModeNone + db.Spec.InitMode = cluster.NoDB // Don't include previous config anymore db.Spec.IncludeConfig = false @@ -831,44 +955,54 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf newcd.Cluster.Spec.PGParameters = db.Status.PGParameters } // Cluster initialized, switch to Normal state - newcd.Cluster.Status.Phase = cluster.ClusterPhaseNormal + newcd.Cluster.Status.Phase = cluster.Normal } case Converging: - log.Infow("waiting for db", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("waiting for db") case ConvergenceFailed: - log.Infow("db failed to initialize", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("db failed to initialize") // Empty DBs newcd.DBs = cluster.DBs{} // Unset master so another keeper can be chosen newcd.Cluster.Status.Master = "" } } - case cluster.ClusterInitModeExisting: + case cluster.ExistingCluster: if cd.Cluster.Status.Master == "" { wantedKeeper := clusterSpec.ExistingConfig.KeeperUID - log.Infow("trying to use keeper as initial master", "keeper", wantedKeeper) + logger.Info(). + Str(logKeeper, wantedKeeper). + Msg("trying to use keeper as initial master") k, ok := newcd.Keepers[wantedKeeper] if !ok { return nil, fmt.Errorf("keeper %q state not available", wantedKeeper) } - log.Infow("initializing cluster using selected keeper as master db owner", "keeper", k.UID) + logger.Info(). + Str(logKeeper, k.UID). + Msg("initializing cluster using selected keeper as master db owner") db := &cluster.DB{ UID: s.UIDFn(), Generation: cluster.InitialGeneration, Spec: &cluster.DBSpec{ KeeperUID: k.UID, - InitMode: cluster.DBInitModeExisting, - Role: common.RoleMaster, + InitMode: cluster.ExistingDB, + Role: common.RolePrimary, Followers: []string{}, IncludeConfig: *clusterSpec.MergePgParameters, }, } newcd.DBs[db.UID] = db newcd.Cluster.Status.Master = db.UID - log.Debugf("newcd dump: %s", spew.Sdump(newcd)) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump") } else { db, ok := newcd.DBs[cd.Cluster.Status.Master] if !ok { @@ -876,9 +1010,11 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } // Check that the choosed db for being the master has correctly initialized if db.Status.Healthy && s.dbConvergenceState(db, clusterSpec.ConvergenceTimeout.Duration) == Converged { - log.Infow("db initialized", "db", db.UID, "keeper", db.Spec.KeeperUID) - // Set db initMode to none, not needed but just a security measure - db.Spec.InitMode = cluster.DBInitModeNone + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("db initialized") // Set db initMode to none, not needed but just a security measure + db.Spec.InitMode = cluster.NoDB // Don't include previous config anymore db.Spec.IncludeConfig = false // Replace reported pg parameters in cluster spec @@ -886,22 +1022,24 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf newcd.Cluster.Spec.PGParameters = db.Status.PGParameters } // Cluster initialized, switch to Normal state - newcd.Cluster.Status.Phase = cluster.ClusterPhaseNormal + newcd.Cluster.Status.Phase = cluster.Normal } } - case cluster.ClusterInitModePITR: + case cluster.PITR: // Is there already a keeper choosed to be the new master? if cd.Cluster.Status.Master == "" { - log.Infow("trying to find initial master") + logger.Info().Msg("trying to find initial master") k, err := s.findInitialKeeper(cd) if err != nil { return nil, fmt.Errorf("cannot choose initial master: %v", err) } - log.Infow("initializing cluster using selected keeper as master db owner", "keeper", k.UID) - role := common.RoleMaster + logger.Info(). + Str(logKeeper, k.UID). + Msg("initializing cluster using selected keeper as master db owner") + role := common.RolePrimary var followConfig *cluster.FollowConfig - if *clusterSpec.Role == cluster.ClusterRoleStandby { - role = common.RoleStandby + if *clusterSpec.Role == cluster.Replica { + role = common.RoleReplica followConfig = &cluster.FollowConfig{ Type: cluster.FollowTypeExternal, StandbySettings: clusterSpec.StandbyConfig.StandbySettings, @@ -913,7 +1051,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf Generation: cluster.InitialGeneration, Spec: &cluster.DBSpec{ KeeperUID: k.UID, - InitMode: cluster.DBInitModePITR, + InitMode: cluster.PITRDB, PITRConfig: clusterSpec.PITRConfig, Role: role, FollowConfig: followConfig, @@ -923,7 +1061,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } newcd.DBs[db.UID] = db newcd.Cluster.Status.Master = db.UID - log.Debugf("newcd dump: %s", spew.Sdump(newcd)) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump") } else { db, ok := newcd.DBs[cd.Cluster.Status.Master] if !ok { @@ -934,9 +1072,12 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf switch s.dbConvergenceState(db, 0) { case Converged: if db.Status.Healthy { - log.Infow("db initialized", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("db initialized") // Set db initMode to none, not needed but just a security measure - db.Spec.InitMode = cluster.DBInitModeNone + db.Spec.InitMode = cluster.NoDB // Don't include previous config anymore db.Spec.IncludeConfig = false @@ -945,12 +1086,18 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf newcd.Cluster.Spec.PGParameters = db.Status.PGParameters } // Cluster initialized, switch to Normal state - newcd.Cluster.Status.Phase = cluster.ClusterPhaseNormal + newcd.Cluster.Status.Phase = cluster.Normal } case Converging: - log.Infow("waiting for db to converge", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("waiting for db to converge") case ConvergenceFailed: - log.Infow("db failed to initialize", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("db failed to initialize") // Empty DBs newcd.DBs = cluster.DBs{} // Unset master so another keeper can be chosen @@ -961,7 +1108,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf return nil, fmt.Errorf("unknown init mode %s", *cd.Cluster.DefSpec().InitMode) } - case cluster.ClusterPhaseNormal: + case cluster.Normal: // Remove old keepers keepersToRemove := []*cluster.Keeper{} for _, k := range newcd.Keepers { @@ -972,7 +1119,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf continue } if time.Now().After(k.Status.LastHealthyTime.Add(cd.Cluster.DefSpec().DeadKeeperRemovalInterval.Duration)) { - log.Infow("removing old dead keeper", "keeper", k.UID) + logger.Info().Str(logKeeper, k.UID).Msg("removing old dead keeper") keepersToRemove = append(keepersToRemove, k) } } @@ -987,33 +1134,47 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf masterOK := true curMasterDB := cd.DBs[curMasterDBUID] if curMasterDB == nil { - return nil, fmt.Errorf("db for keeper %q not available. This shouldn't happen!", curMasterDBUID) + return nil, fmt.Errorf("db for keeper %q not available, this shouldn't happen", curMasterDBUID) } - log.Debugf("db dump: %s", spew.Sdump(curMasterDB)) + logger.Debug().Any("db", spew.Sdump(curMasterDB)).Msg("db dump") if !curMasterDB.Status.Healthy { - log.Infow("master db is failed", "db", curMasterDB.UID, "keeper", curMasterDB.Spec.KeeperUID) + logger.Info(). + Str(logDB, curMasterDB.UID). + Str(logKeeper, curMasterDB.Spec.KeeperUID). + Msg("master db is failed") masterOK = false } // Check that the wanted master is in master state (i.e. check that promotion from standby to master happened) if s.dbConvergenceState(curMasterDB, clusterSpec.ConvergenceTimeout.Duration) == ConvergenceFailed { - log.Infow("db not converged", "db", curMasterDB.UID, "keeper", curMasterDB.Spec.KeeperUID) + logger.Info(). + Str(logDB, curMasterDB.UID). + Str(logKeeper, curMasterDB.Spec.KeeperUID). + Msg("db not converged") masterOK = false } if !masterOK { - log.Infow("trying to find a new master to replace failed master") - bestNewMasters := s.findBestNewMasters(newcd, curMasterDB) + logger.Info().Msg("trying to find a new master to replace failed master") + bestNewMasters := s.findBestNewMasters(ctx, newcd, curMasterDB) if len(bestNewMasters) == 0 { - log.Errorw("no eligible masters") + logger.Error().Msg("no eligible masters") } else { - // if synchronous replication is enabled, only choose new master in the synchronous replication standbys. + // if synchronous replication is enabled, + // only choose new master in the synchronous replication standbys. var bestNewMasterDB *cluster.DB if curMasterDB.Spec.SynchronousReplication { - commonSyncStandbys := util.CommonElements(curMasterDB.Status.SynchronousStandbys, curMasterDB.Spec.SynchronousStandbys) + commonSyncStandbys := util.CommonElements( + curMasterDB.Status.SynchronousStandbys, + curMasterDB.Spec.SynchronousStandbys) if len(commonSyncStandbys) == 0 { - log.Warnw("cannot choose synchronous standby since there are no common elements between the latest master reported synchronous standbys and the db spec ones", "reported", curMasterDB.Status.SynchronousStandbys, "spec", curMasterDB.Spec.SynchronousStandbys) + // revive:disable-next-line + logger.Warn(). + Any("reported", curMasterDB.Status.SynchronousStandbys). + Any("spec", curMasterDB.Spec.SynchronousStandbys). + Msg("cannot choose synchronous standby since there are no common elements " + + "between the latest master reported synchronous standbys and the db spec ones") } else { for _, nm := range bestNewMasters { if util.StringInSlice(commonSyncStandbys, nm.UID) { @@ -1022,17 +1183,27 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } } if bestNewMasterDB == nil { - log.Warnw("cannot choose synchronous standby since there's not match between the possible masters and the usable synchronousStandbys", "reported", curMasterDB.Status.SynchronousStandbys, "spec", curMasterDB.Spec.SynchronousStandbys, "common", commonSyncStandbys, "possibleMasters", bestNewMasters) + // revive:disable-next-line + logger.Warn(). + Any("reported", curMasterDB.Status.SynchronousStandbys). + Any("spec", curMasterDB.Spec.SynchronousStandbys). + Any("common", commonSyncStandbys). + Any("possibleMasters", bestNewMasters). + Msg("cannot choose synchronous standby since there's not match between the possible " + + "masters and the usable synchronousStandbys") } } } else { bestNewMasterDB = bestNewMasters[0] } if bestNewMasterDB != nil { - log.Infow("electing db as the new master", "db", bestNewMasterDB.UID, "keeper", bestNewMasterDB.Spec.KeeperUID) + logger.Info(). + Str(logDB, bestNewMasterDB.UID). + Str(logKeeper, bestNewMasterDB.Spec.KeeperUID). + Msg("electing db as the new master") wantedMasterDBUID = bestNewMasterDB.UID } else { - log.Errorw("no eligible masters") + logger.Error().Msg("no eligible masters") } } } @@ -1043,10 +1214,10 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf oldMasterdb := newcd.DBs[curMasterDBUID] oldMasterdb.Spec.Followers = []string{} - masterDBRole := common.RoleMaster + masterDBRole := common.RolePrimary var followConfig *cluster.FollowConfig - if *clusterSpec.Role == cluster.ClusterRoleStandby { - masterDBRole = common.RoleStandby + if *clusterSpec.Role == cluster.Replica { + masterDBRole = common.RoleReplica followConfig = &cluster.FollowConfig{ Type: cluster.FollowTypeExternal, StandbySettings: clusterSpec.StandbyConfig.StandbySettings, @@ -1073,9 +1244,13 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf newMasterDB.Spec.ExternalSynchronousStandbys = []string{} for _, dbUID := range oldMasterdb.Spec.SynchronousStandbys { if dbUID != newMasterDB.UID { - newMasterDB.Spec.SynchronousStandbys = append(newMasterDB.Spec.SynchronousStandbys, dbUID) + newMasterDB.Spec.SynchronousStandbys = append( + newMasterDB.Spec.SynchronousStandbys, + dbUID) } else { - newMasterDB.Spec.SynchronousStandbys = append(newMasterDB.Spec.SynchronousStandbys, oldMasterdb.UID) + newMasterDB.Spec.SynchronousStandbys = append( + newMasterDB.Spec.SynchronousStandbys, + oldMasterdb.UID) } } if len(newMasterDB.Spec.SynchronousStandbys) == 0 && *clusterSpec.MinSynchronousStandbys > 0 { @@ -1109,7 +1284,9 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } } if len(unconvergedProxiesUIDs) > 0 { - log.Infow("waiting for proxies to be converged to the current generation", "proxies", unconvergedProxiesUIDs) + logger.Info(). + Any("proxies", unconvergedProxiesUIDs). + Msg("waiting for proxies to be converged to the current generation") } else { // Tell proxy that there's a new active master newcd.Proxy.Spec.MasterDBUID = wantedMasterDBUID @@ -1131,14 +1308,15 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } // change master db role to "master" if the cluster role has been changed in the spec - if *clusterSpec.Role == cluster.ClusterRoleMaster { - masterDB.Spec.Role = common.RoleMaster + if *clusterSpec.Role == cluster.Primary { + masterDB.Spec.Role = common.RolePrimary masterDB.Spec.FollowConfig = nil } // Set standbys to follow master only if it's healthy and converged - if masterDB.Status.Healthy && s.dbConvergenceState(masterDB, clusterSpec.ConvergenceTimeout.Duration) == Converged { - + if masterDB.Status.Healthy && s.dbConvergenceState( + masterDB, + clusterSpec.ConvergenceTimeout.Duration) == Converged { // Remove old masters toRemove := []*cluster.DB{} for _, db := range newcd.DBs { @@ -1148,7 +1326,10 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf if s.dbType(newcd, db.UID) != dbTypeMaster { continue } - log.Infow("removing old master db", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("removing old master db") toRemove = append(toRemove, db) } for _, db := range toRemove { @@ -1161,10 +1342,13 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf if db.UID == wantedMasterDBUID { continue } - if s.dbValidity(newcd, db.UID) != dbValidityInvalid { + if s.dbValidity(ctx, newcd, db.UID) != dbValidityInvalid { continue } - log.Infow("removing invalid db", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("removing invalid db") toRemove = append(toRemove, db) } for _, db := range toRemove { @@ -1174,25 +1358,32 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf // Remove dbs that won't sync due to missing wals on current master toRemove = []*cluster.DB{} for _, db := range newcd.DBs { - if s.dbCanSync(cd, db.UID) { + if s.dbCanSync(ctx, cd, db.UID) { continue } - log.Infow("removing db that won't be able to sync due to missing wals on current master", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("removing db that won't be able to sync due to missing wals on current master") toRemove = append(toRemove, db) } for _, db := range toRemove { delete(newcd.DBs, db.UID) } - goodStandbys, failedStandbys, convergingStandbys := s.validStandbysByStatus(newcd) + goodStandbys, failedStandbys, convergingStandbys := s.validStandbysByStatus(ctx, newcd) goodStandbysCount := len(goodStandbys) failedStandbysCount := len(failedStandbys) convergingStandbysCount := len(convergingStandbys) - log.Debugw("standbys states", "good", goodStandbysCount, "failed", failedStandbysCount, "converging", convergingStandbysCount) + logger.Debug(). + Int("good", goodStandbysCount). + Int("failed", failedStandbysCount). + Int("converging", convergingStandbysCount). + Msg("standbys states") // Clean InitMode for goodStandbys for _, db := range goodStandbys { - db.Spec.InitMode = cluster.DBInitModeNone + db.Spec.InitMode = cluster.NoDB } // Setup synchronous standbys @@ -1211,42 +1402,73 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf // synchronous standby as a new primary if it's not yet in // sync if masterDBKeeper.Status.PostgresBinaryVersion.Maj != 0 { - if masterDBKeeper.Status.PostgresBinaryVersion.Maj == 9 && masterDBKeeper.Status.PostgresBinaryVersion.Min <= 5 { + pgVersionString := fmt.Sprintf("%d.%d", + masterDBKeeper.Status.PostgresBinaryVersion.Maj, + masterDBKeeper.Status.PostgresBinaryVersion.Min, + ) + pgVersion, err := semver.NewVersion(pgVersionString) + if err != nil { + return nil, fmt.Errorf("unable to convert to semver: %s", pgVersionString) + } + + if pgVersion.LessThanEqual(postgresql.V95) { minSynchronousStandbys = 1 maxSynchronousStandbys = 1 merge = false } } - // if the current known in sync syncstandbys are different than the required ones wait for them and remove non good ones - if !util.CompareStringSliceNoOrder(masterDB.Status.SynchronousStandbys, masterDB.Spec.SynchronousStandbys) { - + // if the current known in sync syncstandbys are different than the required ones, + // wait for them and remove non good ones + if !util.CompareStringSliceNoOrder( + masterDB.Status.SynchronousStandbys, + masterDB.Spec.SynchronousStandbys) { // remove old syncstandbys from current status - masterDB.Status.SynchronousStandbys = util.CommonElements(masterDB.Status.SynchronousStandbys, masterDB.Spec.SynchronousStandbys) + masterDB.Status.SynchronousStandbys = util.CommonElements( + masterDB.Status.SynchronousStandbys, + masterDB.Spec.SynchronousStandbys) // add reported in sync syncstandbys to the current status - curSyncStandbys := util.CommonElements(masterDB.Status.CurSynchronousStandbys, masterDB.Spec.SynchronousStandbys) + curSyncStandbys := util.CommonElements( + masterDB.Status.CurSynchronousStandbys, + masterDB.Spec.SynchronousStandbys) toAddSyncStandbys := util.Difference(curSyncStandbys, masterDB.Status.SynchronousStandbys) - masterDB.Status.SynchronousStandbys = append(masterDB.Status.SynchronousStandbys, toAddSyncStandbys...) - - // if some of the non yet in sync syncstandbys are failed, set Spec.SynchronousStandbys to the current in sync ones, se other could be added. - notInSyncSyncStandbys := util.Difference(masterDB.Spec.SynchronousStandbys, masterDB.Status.SynchronousStandbys) + masterDB.Status.SynchronousStandbys = append( + masterDB.Status.SynchronousStandbys, + toAddSyncStandbys...) + + // if some of the non yet in sync syncstandbys are failed, + // set Spec.SynchronousStandbys to the current in sync ones, so other could be added. + notInSyncSyncStandbys := util.Difference( + masterDB.Spec.SynchronousStandbys, + masterDB.Status.SynchronousStandbys) update := false for _, dbUID := range notInSyncSyncStandbys { if _, ok := newcd.DBs[dbUID]; !ok { - log.Infow("one of the new synchronousStandbys has been removed", "db", dbUID, "inSyncStandbys", masterDB.Status.SynchronousStandbys, "synchronousStandbys", masterDB.Spec.SynchronousStandbys) + logger.Info(). + Str(logDB, dbUID). + Any("inSyncStandbys", masterDB.Status.SynchronousStandbys). + Any(logSyncStdbys, masterDB.Spec.SynchronousStandbys). + Msg("one of the new synchronousStandbys has been removed") update = true continue } if _, ok := goodStandbys[dbUID]; !ok { - log.Infow("one of the new synchronousStandbys is not in good state", "db", dbUID, "inSyncStandbys", masterDB.Status.SynchronousStandbys, "synchronousStandbys", masterDB.Spec.SynchronousStandbys) + logger.Info(). + Str(logDB, dbUID). + Any("inSyncStandbys", masterDB.Status.SynchronousStandbys). + Any(logSyncStdbys, masterDB.Spec.SynchronousStandbys). + Msg("one of the new synchronousStandbys is not in good state") update = true continue } } if update { // Use the current known in sync syncStandbys as Spec.SynchronousStandbys - log.Infow("setting the expected sync-standbys to the current known in sync sync-standbys", "inSyncStandbys", masterDB.Status.SynchronousStandbys, "synchronousStandbys", masterDB.Spec.SynchronousStandbys) + logger.Info(). + Any("inSyncStandbys", masterDB.Status.SynchronousStandbys). + Any(logSyncStdbys, masterDB.Spec.SynchronousStandbys). + Msg("setting the expected sync-standbys to the current known in sync sync-standbys") masterDB.Spec.SynchronousStandbys = masterDB.Status.SynchronousStandbys // Just sort to always have them in the same order and avoid @@ -1260,8 +1482,13 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf // this way, when we have to choose a new master we are sure // that there're no intermediate changes between the // reported standbys and the required ones. - if !util.CompareStringSliceNoOrder(masterDB.Status.SynchronousStandbys, masterDB.Spec.SynchronousStandbys) { - log.Infow("waiting for new defined synchronous standbys to be in sync", "inSyncStandbys", curMasterDB.Status.SynchronousStandbys, "synchronousStandbys", curMasterDB.Spec.SynchronousStandbys) + if !util.CompareStringSliceNoOrder( + masterDB.Status.SynchronousStandbys, + masterDB.Spec.SynchronousStandbys) { + logger.Info(). + Any("inSyncStandbys", curMasterDB.Status.SynchronousStandbys). + Any(logSyncStdbys, curMasterDB.Spec.SynchronousStandbys). + Msg("waiting for new defined synchronous standbys to be in sync") } else { addFakeStandby := false externalSynchronousStandbys := map[string]struct{}{} @@ -1279,7 +1506,10 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf toRemove := map[string]struct{}{} for dbUID := range synchronousStandbys { if _, ok := newcd.DBs[dbUID]; !ok { - log.Infow("removing non existent db from synchronousStandbys", "masterDB", masterDB.UID, "db", dbUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logDB, dbUID). + Msg("removing non existent db from synchronousStandbys") toRemove[dbUID] = struct{}{} } } @@ -1291,7 +1521,10 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf toRemove = map[string]struct{}{} for dbUID := range synchronousStandbys { if _, ok := goodStandbys[dbUID]; !ok { - log.Infow("removing failed synchronous standby", "masterDB", masterDB.UID, "db", dbUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logDB, dbUID). + Msg("removing failed synchronous standby") toRemove[dbUID] = struct{}{} } } @@ -1308,7 +1541,10 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf if removedCount >= rc { break } - log.Infow("removing synchronous standby in excess", "masterDB", masterDB.UID, "db", dbUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logDB, dbUID). + Msg("removing synchronous standby in excess") toRemove[dbUID] = struct{}{} removedCount++ } @@ -1318,7 +1554,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } // try to add missing standbys up to MaxSynchronousStandbys - bestStandbys := s.findBestStandbys(newcd, curMasterDB) + bestStandbys := s.findBestStandbys(ctx, newcd, curMasterDB) ac := maxSynchronousStandbys - len(synchronousStandbys) addedCount := 0 @@ -1332,13 +1568,23 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf // ignore standbys that cannot be synchronous standbys if db, ok := newcd.DBs[bestStandby.UID]; ok { - if keeper, ok := newcd.Keepers[db.Spec.KeeperUID]; ok && (keeper.Status.CanBeSynchronousReplica != nil && !*keeper.Status.CanBeSynchronousReplica) { - log.Infow("cannot choose standby as synchronous (--can-be-synchronous-replica=false)", "db", db.UID, "keeper", keeper.UID) + if keeper, ok := newcd.Keepers[db.Spec.KeeperUID]; ok && + (keeper.Status.CanBeSynchronousReplica != nil && + !*keeper.Status.CanBeSynchronousReplica) { + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, keeper.UID). + Msg("cannot choose standby as synchronous (--can-be-synchronous-replica=false)") continue } } - log.Infow("adding new synchronous standby in good state trying to reach MaxSynchronousStandbys", "masterDB", masterDB.UID, "synchronousStandbyDB", bestStandby.UID, "keeper", bestStandby.Spec.KeeperUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logSyncStdbyDb, bestStandby.UID). + Str(logKeeper, bestStandby.Spec.KeeperUID). + Msg("adding new synchronous standby in good state trying to reach " + + "MaxSynchronousStandbys") synchronousStandbys[bestStandby.UID] = struct{}{} addedCount++ } @@ -1358,7 +1604,11 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf continue } if _, ok := prevSynchronousStandbys[db.UID]; ok { - log.Infow("adding previous synchronous standby to reach MinSynchronousStandbys", "masterDB", masterDB.UID, "synchronousStandbyDB", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logSyncStdbyDb, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("adding previous synchronous standby to reach MinSynchronousStandbys") synchronousStandbys[db.UID] = struct{}{} addedCount++ } @@ -1382,11 +1632,19 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } } if !allInPrev { - log.Infow("merging current and previous synchronous standbys", "masterDB", masterDB.UID, "prevSynchronousStandbys", prevSynchronousStandbys, "synchronousStandbys", synchronousStandbys) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Any(logPrevSyncStdbys, prevSynchronousStandbys). + Any(logSyncStdbys, synchronousStandbys). + Msg("merging current and previous synchronous standbys") // use only existing dbs for _, db := range newcd.DBs { if _, ok := prevSynchronousStandbys[db.UID]; ok { - log.Infow("adding previous synchronous standby", "masterDB", masterDB.UID, "synchronousStandbyDB", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Str(logSyncStdbyDb, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("adding previous synchronous standby") synchronousStandbys[db.UID] = struct{}{} } } @@ -1394,14 +1652,28 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf } if !reflect.DeepEqual(synchronousStandbys, prevSynchronousStandbys) { - log.Infow("synchronousStandbys changed", "masterDB", masterDB.UID, "prevSynchronousStandbys", prevSynchronousStandbys, "synchronousStandbys", synchronousStandbys) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Any(logPrevSyncStdbys, prevSynchronousStandbys). + Any(logSyncStdbys, synchronousStandbys). + Msg("synchronousStandbys changed") } else { - log.Debugf("synchronousStandbys not changed", "masterDB", masterDB.UID, "prevSynchronousStandbys", prevSynchronousStandbys, "synchronousStandbys", synchronousStandbys) + logger.Debug(). + Str(logMasterDB, masterDB.UID). + Any(logPrevSyncStdbys, prevSynchronousStandbys). + Any(logSyncStdbys, synchronousStandbys). + Msg("synchronousStandbys not changed") } - // If there're not enough real synchronous standbys add a fake synchronous standby because we have to be strict and make the master block transactions until MinSynchronousStandbys real standbys are available + // If there're not enough real synchronous standbys add a fake synchronous standby + // because we have to be strict and make the master block transactions + // until MinSynchronousStandbys real standbys are available if len(synchronousStandbys)+len(externalSynchronousStandbys) < minSynchronousStandbys { - log.Infow("using a fake synchronous standby since there are not enough real standbys available", "masterDB", masterDB.UID, "required", minSynchronousStandbys) + logger.Info(). + Str(logMasterDB, masterDB.UID). + Int("required", minSynchronousStandbys). + Msg("using a fake synchronous standby since there are not enough real standbys " + + "available") addFakeStandby = true } @@ -1409,19 +1681,27 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf masterDB.Spec.SynchronousStandbys = []string{} masterDB.Spec.ExternalSynchronousStandbys = []string{} for dbUID := range synchronousStandbys { - masterDB.Spec.SynchronousStandbys = append(masterDB.Spec.SynchronousStandbys, dbUID) + masterDB.Spec.SynchronousStandbys = append( + masterDB.Spec.SynchronousStandbys, + dbUID) } for dbUID := range externalSynchronousStandbys { - masterDB.Spec.ExternalSynchronousStandbys = append(masterDB.Spec.ExternalSynchronousStandbys, dbUID) + masterDB.Spec.ExternalSynchronousStandbys = append( + masterDB.Spec.ExternalSynchronousStandbys, + dbUID) } if addFakeStandby { - masterDB.Spec.ExternalSynchronousStandbys = append(masterDB.Spec.ExternalSynchronousStandbys, fakeStandbyName) + masterDB.Spec.ExternalSynchronousStandbys = append( + masterDB.Spec.ExternalSynchronousStandbys, + fakeStandbyName) } // remove old syncstandbys from current status - masterDB.Status.SynchronousStandbys = util.CommonElements(masterDB.Status.SynchronousStandbys, masterDB.Spec.SynchronousStandbys) + masterDB.Status.SynchronousStandbys = util.CommonElements( + masterDB.Status.SynchronousStandbys, + masterDB.Spec.SynchronousStandbys) // Just sort to always have them in the same order and avoid // unneeded updates to synchronous_standby_names by the keeper. @@ -1440,9 +1720,9 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf // it's the total number of standbys - the failed standbys // or the sum of good + converging standbys notFailedStandbysCount := goodStandbysCount + convergingStandbysCount - // Remove dbs in excess if we have a good number >= MaxStandbysPerSender - // We don't remove failed db until the number of good db is >= MaxStandbysPerSender since they can come back + // We don't remove failed db until the number of good db is >= MaxStandbysPerSender + // since they can come back if uint16(goodStandbysCount) >= *clusterSpec.MaxStandbysPerSender { toRemove := []*cluster.DB{} // Remove all non good standbys @@ -1455,7 +1735,7 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf continue } if _, ok := goodStandbys[db.UID]; !ok { - log.Infow("removing non good standby", "db", db.UID) + logger.Info().Str(logDB, db.UID).Msg("removing non good standby") toRemove = append(toRemove, db) } } @@ -1470,14 +1750,13 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf if util.StringInSlice(masterDB.Spec.SynchronousStandbys, db.UID) { continue } - log.Infow("removing good standby in excess", "db", db.UID) + logger.Info().Str(logDB, db.UID).Msg("removing good standby in excess") toRemove = append(toRemove, db) i++ } for _, db := range toRemove { delete(newcd.DBs, db.UID) } - } else { // Add new dbs to substitute failed dbs, if there're available keepers. @@ -1492,15 +1771,20 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf UID: s.UIDFn(), Generation: cluster.InitialGeneration, Spec: &cluster.DBSpec{ - KeeperUID: freeKeeper.UID, - InitMode: cluster.DBInitModeResync, - Role: common.RoleStandby, - Followers: []string{}, - FollowConfig: &cluster.FollowConfig{Type: cluster.FollowTypeInternal, DBUID: wantedMasterDBUID}, + KeeperUID: freeKeeper.UID, + InitMode: cluster.ResyncDB, + Role: common.RoleReplica, + Followers: []string{}, + FollowConfig: &cluster.FollowConfig{ + Type: cluster.FollowTypeInternal, + DBUID: wantedMasterDBUID}, }, } newcd.DBs[db.UID] = db - log.Infow("added new standby db", "db", db.UID, "keeper", db.Spec.KeeperUID) + logger.Info(). + Str(logDB, db.UID). + Str(logKeeper, db.Spec.KeeperUID). + Msg("added new standby db") } } @@ -1510,10 +1794,12 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf continue } - db.Spec.Role = common.RoleStandby + db.Spec.Role = common.RoleReplica // Remove followers db.Spec.Followers = []string{} - db.Spec.FollowConfig = &cluster.FollowConfig{Type: cluster.FollowTypeInternal, DBUID: wantedMasterDBUID} + db.Spec.FollowConfig = &cluster.FollowConfig{ + Type: cluster.FollowTypeInternal, + DBUID: wantedMasterDBUID} db.Spec.SynchronousReplication = false db.Spec.SynchronousStandbys = nil @@ -1556,19 +1842,22 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf continue } if !reflect.DeepEqual(db.Spec, prevDB.Spec) { - log.Debugw("db spec changed, updating generation", "prevDB", spew.Sdump(prevDB.Spec), "db", spew.Sdump(db.Spec)) + logger.Debug(). + Any("prevDB", spew.Sdump(prevDB.Spec)). + Any(logDB, spew.Sdump(db.Spec)). + Msg("db spec changed, updating generation") db.Generation++ } } // check that we haven't changed the current cd or there's a bug somewhere if !reflect.DeepEqual(origcd, cd) { - return nil, fmt.Errorf("cd was changed in updateCluster, this shouldn't happen!") + return nil, errors.New("cd was changed in updateCluster, this shouldn't happen") } return newcd, nil } -func (s *Sentinel) updateChangeTimes(cd, newcd *cluster.ClusterData) { +func (s *Sentinel) updateChangeTimes(cd, newcd *cluster.Data) { newcd.ChangeTime = time.Now() if !reflect.DeepEqual(newcd.Cluster, cd.Cluster) { @@ -1602,15 +1891,19 @@ func (s *Sentinel) updateChangeTimes(cd, newcd *cluster.ClusterData) { } } +// ConvergenceState is an ENUM for convergece states type ConvergenceState uint const ( + // Converging means that it is converging Converging ConvergenceState = iota + // Converged means that convergence has finished Converged + // ConvergenceFailed means that converging has run into an issue ConvergenceFailed ) -func (s *Sentinel) isKeeperHealthy(cd *cluster.ClusterData, keeper *cluster.Keeper) bool { +func (s *Sentinel) isKeeperHealthy(cd *cluster.Data, keeper *cluster.Keeper) bool { t, ok := s.keeperErrorTimers[keeper.UID] if !ok { return true @@ -1621,7 +1914,7 @@ func (s *Sentinel) isKeeperHealthy(cd *cluster.ClusterData, keeper *cluster.Keep return true } -func (s *Sentinel) isDBHealthy(cd *cluster.ClusterData, db *cluster.DB) bool { +func (s *Sentinel) isDBHealthy(cd *cluster.Data, db *cluster.DB) bool { t, ok := s.dbErrorTimers[db.UID] if !ok { return true @@ -1632,7 +1925,7 @@ func (s *Sentinel) isDBHealthy(cd *cluster.ClusterData, db *cluster.DB) bool { return true } -func (s *Sentinel) isDBIncreasingXLogPos(cd *cluster.ClusterData, db *cluster.DB) bool { +func (s *Sentinel) isDBIncreasingXLogPos(db *cluster.DB) bool { t, ok := s.dbNotIncreasingXLogPos[db.UID] if !ok { return true @@ -1643,7 +1936,7 @@ func (s *Sentinel) isDBIncreasingXLogPos(cd *cluster.ClusterData, db *cluster.DB return true } -func (s *Sentinel) updateDBConvergenceInfos(cd *cluster.ClusterData) { +func (s *Sentinel) updateDBConvergenceInfos(cd *cluster.Data) { for _, db := range cd.DBs { if db.Status.CurrentGeneration == db.Generation { delete(s.dbConvergenceInfos, db.UID) @@ -1666,7 +1959,7 @@ func (s *Sentinel) dbConvergenceState(db *cluster.DB, timeout time.Duration) Con if timeout != 0 { d, ok := s.dbConvergenceInfos[db.UID] if !ok { - panic(fmt.Errorf("no db convergence info for db %q, this shouldn't happen!", db.UID)) + panic(fmt.Errorf("no db convergence info for db %q, this shouldn't happen", db.UID)) } if timer.Since(d.Timer) > timeout { return ConvergenceFailed @@ -1675,54 +1968,65 @@ func (s *Sentinel) dbConvergenceState(db *cluster.DB, timeout time.Duration) Con return Converging } +// KeeperInfoHistory tracks the states of a keeper type KeeperInfoHistory struct { KeeperInfo *cluster.KeeperInfo Seen bool Timer int64 } +// KeeperInfoHistories tracks info of all keepers of this cluster type KeeperInfoHistories map[string]*KeeperInfoHistory -func (k KeeperInfoHistories) DeepCopy() KeeperInfoHistories { +// DeepCopy returns a copy of all KeeperInfoHistories, where every KeeperInfoHistory is also copied +func (k KeeperInfoHistories) DeepCopy() (nk KeeperInfoHistories) { if k == nil { return nil } - nk, err := copystructure.Copy(k) - if err != nil { + var ok bool + if kihCopy, err := copystructure.Copy(k); err != nil { panic(err) - } - if !reflect.DeepEqual(k, nk) { + } else if !reflect.DeepEqual(k, kihCopy) { panic("not equal") + } else if nk, ok = kihCopy.(KeeperInfoHistories); !ok { + panic("unexpectdly, copy is not same type") } - return nk.(KeeperInfoHistories) + return nk } +// DBConvergenceInfo stores convergence info of a cluster for a sentinel type DBConvergenceInfo struct { Generation int64 Timer int64 } +// ProxyInfoHistory is one record in ProxyInfoHistories, whichh holds a record of ProxyInfo's type ProxyInfoHistory struct { ProxyInfo *cluster.ProxyInfo Timer int64 } +// ProxyInfoHistories is a map of ProxyInfoHistory items type ProxyInfoHistories map[string]*ProxyInfoHistory -func (p ProxyInfoHistories) DeepCopy() ProxyInfoHistories { +// DeepCopy returns a deep copy of ProxyInfoHistories which is a new ProxyInfoHistories with a copy of all +// ProxyInfoHistory items +func (p ProxyInfoHistories) DeepCopy() (cp ProxyInfoHistories) { if p == nil { return nil } - np, err := copystructure.Copy(p) - if err != nil { + var ok bool + if np, err := copystructure.Copy(p); err != nil { panic(err) - } - if !reflect.DeepEqual(p, np) { + } else if !reflect.DeepEqual(p, np) { panic("not equal") + } else if cp, ok = np.(ProxyInfoHistories); !ok { + panic("unexpectedly copy is not same type") } - return np.(ProxyInfoHistories) + return cp } +// Sentinel is a sttruct to keep all Sentinel info type Sentinel struct { uid string cfg *config @@ -1739,7 +2043,7 @@ type Sentinel struct { leadershipCount uint leaderMutex sync.Mutex - initialClusterSpec *cluster.ClusterSpec + initialClusterSpec *cluster.Spec sleepInterval time.Duration requestTimeout time.Duration @@ -1758,28 +2062,30 @@ type Sentinel struct { proxyInfoHistories ProxyInfoHistories } -func NewSentinel(uid string, cfg *config, end chan bool) (*Sentinel, error) { - var initialClusterSpec *cluster.ClusterSpec +// NewSentinel retruns a new Sentinel object +func NewSentinel(ctx context.Context, uid string, cfg *config, end chan bool) (*Sentinel, error) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) + var initialClusterSpec *cluster.Spec if cfg.initialClusterSpecFile != "" { - configData, err := ioutil.ReadFile(cfg.initialClusterSpecFile) + configData, err := os.ReadFile(cfg.initialClusterSpecFile) if err != nil { return nil, fmt.Errorf("cannot read provided initial cluster config file: %v", err) } if err := json.Unmarshal(configData, &initialClusterSpec); err != nil { return nil, fmt.Errorf("cannot parse provided initial cluster config: %v", err) } - log.Debugw("initialClusterSpec dump", "initialClusterSpec", spew.Sdump(initialClusterSpec)) + logger.Debug().Any("initialClusterSpec", spew.Sdump(initialClusterSpec)).Msg("initialClusterSpec dump") if err := initialClusterSpec.Validate(); err != nil { return nil, fmt.Errorf("invalid initial cluster: %v", err) } } - e, err := cmd.NewStore(&cfg.CommonConfig) + e, err := cmd.NewStore(ctx, &cfg.CommonConfig) if err != nil { return nil, fmt.Errorf("cannot create store: %v", err) } - election, err := cmd.NewElection(&cfg.CommonConfig, uid) + election, err := cmd.NewElection(ctx, &cfg.CommonConfig, uid) if err != nil { return nil, fmt.Errorf("cannot create election: %v", err) } @@ -1803,7 +2109,9 @@ func NewSentinel(uid string, cfg *config, end chan bool) (*Sentinel, error) { }, nil } +// Start starts a Sentinel func (s *Sentinel) Start(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) endCh := make(chan struct{}) timerCh := time.NewTimer(0).C @@ -1813,7 +2121,7 @@ func (s *Sentinel) Start(ctx context.Context) { for { select { case <-ctx.Done(): - log.Infow("stopping stolon sentinel") + logger.Info().Msg("stopping stolon sentinel") s.end <- true return case <-timerCh: @@ -1833,23 +2141,27 @@ func (s *Sentinel) leaderInfo() (bool, uint) { return s.leader, s.leadershipCount } -func (s *Sentinel) clusterSentinelCheck(pctx context.Context) { +func (s *Sentinel) clusterSentinelCheck(ctx context.Context) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) s.updateMutex.Lock() defer s.updateMutex.Unlock() e := s.e - cd, prevCDPair, err := e.GetClusterData(pctx) + cd, prevCDPair, err := e.GetClusterData(ctx) if err != nil { - log.Errorw("error retrieving cluster data", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("error retrieving cluster data") return } if cd != nil { if cd.FormatVersion != cluster.CurrentCDFormatVersion { - log.Errorw("unsupported clusterdata format version", "version", cd.FormatVersion) + logger.Error(). + AnErr("err", err). + Uint64("version", cd.FormatVersion). + Msg("unsupported clusterdata format version") return } if err = cd.Cluster.Spec.Validate(); err != nil { - log.Errorw("clusterdata validation failed", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("clusterdata validation failed") return } if cd.Cluster != nil { @@ -1858,39 +2170,39 @@ func (s *Sentinel) clusterSentinelCheck(pctx context.Context) { } } - log.Debugf("cd dump: %s", spew.Sdump(cd)) + logger.Debug().Any("cd", spew.Sdump(cd)).Msg("cd dump") if cd == nil { // Cluster first initialization if s.initialClusterSpec == nil { - log.Infow("no cluster data available, waiting for it to appear") + logger.Info().Msg("no cluster data available, waiting for it to appear") return } c := cluster.NewCluster(s.UIDFn(), s.initialClusterSpec) - log.Infow("writing initial cluster data") + logger.Info().Msg("writing initial cluster data") newcd := cluster.NewClusterData(c) - log.Debugf("newcd dump: %s", spew.Sdump(newcd)) - if _, err = e.AtomicPutClusterData(pctx, newcd, nil); err != nil { - log.Errorw("error saving cluster data", zap.Error(err)) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump") + if _, err = e.AtomicPutClusterData(ctx, newcd, nil); err != nil { + logger.Error().AnErr("err", err).Msg("error saving cluster data") } return } - if err = s.setSentinelInfo(pctx, 2*s.sleepInterval); err != nil { - log.Errorw("cannot update sentinel info", zap.Error(err)) + if err = s.setSentinelInfo(ctx, 2*s.sleepInterval); err != nil { + logger.Error().AnErr("err", err).Msg("cannot update sentinel info") return } - keepersInfo, err := s.e.GetKeepersInfo(pctx) + keepersInfo, err := s.e.GetKeepersInfo(ctx) if err != nil { - log.Errorw("cannot get keepers info", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("cannot get keepers info") return } - log.Debugf("keepersInfo dump: %s", spew.Sdump(keepersInfo)) + logger.Debug().Any("keepersInfo", spew.Sdump(keepersInfo)).Msg("keepersInfo dump") - proxiesInfo, err := s.e.GetProxiesInfo(pctx) + proxiesInfo, err := s.e.GetProxiesInfo(ctx) if err != nil { - log.Errorw("failed to get proxies info", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to get proxies info") return } @@ -1909,33 +2221,33 @@ func (s *Sentinel) clusterSentinelCheck(pctx context.Context) { // if this is the first check after (re)gaining leadership reset all // the internal timers if firstRun { - s.keeperErrorTimers = make(map[string]int64) - s.dbErrorTimers = make(map[string]int64) - s.dbNotIncreasingXLogPos = make(map[string]int64) - s.keeperInfoHistories = make(KeeperInfoHistories) - s.dbConvergenceInfos = make(map[string]*DBConvergenceInfo) - s.proxyInfoHistories = make(ProxyInfoHistories) + s.keeperErrorTimers = map[string]int64{} + s.dbErrorTimers = map[string]int64{} + s.dbNotIncreasingXLogPos = map[string]int64{} + s.keeperInfoHistories = KeeperInfoHistories{} + s.dbConvergenceInfos = map[string]*DBConvergenceInfo{} + s.proxyInfoHistories = ProxyInfoHistories{} // Update db convergence timers since its the first run s.updateDBConvergenceInfos(cd) } - newcd, newKeeperInfoHistories := s.updateKeepersStatus(cd, keepersInfo, firstRun) - log.Debugf("newcd dump after updateKeepersStatus: %s", spew.Sdump(newcd)) + newcd, newKeeperInfoHistories := s.updateKeepersStatus(ctx, cd, keepersInfo, firstRun) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump after updateKeepersStatus") activeProxiesInfos := s.activeProxiesInfos(proxiesInfo) - newcd, err = s.updateCluster(newcd, activeProxiesInfos) + newcd, err = s.updateCluster(ctx, newcd, activeProxiesInfos) if err != nil { - log.Errorw("failed to update cluster data", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("failed to update cluster data") return } - log.Debugf("newcd dump after updateCluster: %s", spew.Sdump(newcd)) + logger.Debug().Any("newcd", spew.Sdump(newcd)).Msg("newcd dump after updateCluster") if newcd != nil { s.updateChangeTimes(cd, newcd) - if _, err := e.AtomicPutClusterData(pctx, newcd, prevCDPair); err != nil { - log.Errorw("error saving clusterdata", zap.Error(err)) + if _, err := e.AtomicPutClusterData(ctx, newcd, prevCDPair); err != nil { + logger.Error().AnErr("err", err).Msg("error saving clusterdata") } } @@ -1953,72 +2265,64 @@ func (s *Sentinel) clusterSentinelCheck(pctx context.Context) { lastCheckSuccessSeconds.SetToCurrentTime() } -func sigHandler(sigs chan os.Signal, cancel context.CancelFunc) { +func sigHandler(ctx context.Context, sigs chan os.Signal, cancel context.CancelFunc) { + _, logger := logging.GetLogComponent(ctx, logging.SentinelComponent) s := <-sigs - log.Debugw("got signal", "signal", s) + logger.Debug().Any("signal", s).Msg("got signal") cancel() } +// Execute is the main execute function of the sentinel file func Execute() { + _, logger := logging.GetLogComponent(context.Background(), logging.SentinelComponent) if err := flagutil.SetFlagsFromEnv(CmdSentinel.PersistentFlags(), "STSENTINEL"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } if err := CmdSentinel.Execute(); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } } -func sentinel(c *cobra.Command, args []string) { - switch cfg.LogLevel { - case "error": - slog.SetLevel(zap.ErrorLevel) - case "warn": - slog.SetLevel(zap.WarnLevel) - case "info": - slog.SetLevel(zap.InfoLevel) - case "debug": - slog.SetLevel(zap.DebugLevel) - default: - log.Fatalf("invalid log level: %v", cfg.LogLevel) - } +func sentinel(c *cobra.Command, _ []string) { + ctx, logger := logging.GetLogComponent(context.Background(), logging.SentinelComponent) + logging.SetStaticLevel(cfg.LogLevel) if cfg.debug { - slog.SetDebug() + logging.SetStaticLevel("debug") } if cmd.IsColorLoggerEnable(c, &cfg.CommonConfig) { - log = slog.SColor() - pg.SetLogger(log) + logging.EnableColor() } if err := cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { - log.Fatalf(err.Error()) + logger.Fatal().AnErr("err", err).Msg("") } cmd.SetMetrics(&cfg.CommonConfig, "sentinel") uid := common.UID() - log.Infow("sentinel uid", "uid", uid) + logger.Info().Str("uid", uid).Msg("sentinel uid") ctx, cancel := context.WithCancel(context.Background()) end := make(chan bool) sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go sigHandler(sigs, cancel) + go sigHandler(ctx, sigs, cancel) if cfg.MetricsListenAddress != "" { http.Handle("/metrics", promhttp.Handler()) go func() { err := http.ListenAndServe(cfg.MetricsListenAddress, nil) if err != nil { - log.Errorw("metrics http server error", zap.Error(err)) + logger.Error().AnErr("err", err).Msg("metrics http server error") cancel() } }() } - s, err := NewSentinel(uid, &cfg, end) + s, err := NewSentinel(ctx, uid, &cfg, end) if err != nil { - log.Fatalf("cannot create sentinel: %v", err) + logger.Fatal().AnErr("err", err).Msg("cannot create sentinel") } go s.Start(ctx) diff --git a/cmd/sentinel/cmd/sentinel_test.go b/cmd/sentinel/cmd/sentinel_test.go index 16a988cd4..e630c2d01 100644 --- a/cmd/sentinel/cmd/sentinel_test.go +++ b/cmd/sentinel/cmd/sentinel_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +16,8 @@ package cmd import ( + "context" + "errors" "fmt" "reflect" "strconv" @@ -22,9 +25,10 @@ import ( "testing" "time" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/timer" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/timer" + "github.com/pgvillage-tools/stolon/internal/util" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" @@ -32,101 +36,125 @@ import ( var curUID int +var newCluster = cluster.New + var now = time.Now() func TestUpdateCluster(t *testing.T) { + const ( + p1 = "param01" + v1 = "value01" + p2 = "param02" + v2 = "value02" + + k1 = "keeper1" + k2 = "keeper2" + k3 = "keeper3" + + c1 = "cluster1" + + d1 = "db1" + d2 = "db2" + d3 = "db3" + + g1 = 1 + g2 = 2 + g3 = 3 + g4 = 4 + ) + ctx := context.Background() tests := []struct { - cd *cluster.ClusterData - outcd *cluster.ClusterData + cd *cluster.Data + outcd *cluster.Data err error }{ // Init phase, also test dbSpec parameters copied from clusterSpec. // #0 cluster initialization, no keepers { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, + CurrentGeneration: g1, + Phase: cluster.Initializing, }, }, Keepers: cluster.Keepers{}, DBs: cluster.DBs{}, Proxy: &cluster.Proxy{}, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, + CurrentGeneration: g1, + Phase: cluster.Initializing, }, }, Keepers: cluster.Keepers{}, DBs: cluster.DBs{}, Proxy: &cluster.Proxy{}, }, - err: fmt.Errorf("cannot choose initial master: no keepers registered"), + err: errors.New("cannot choose initial master: no keepers registered"), }, // #1 cluster initialization, one keeper { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, + CurrentGeneration: g1, + Phase: cluster.Initializing, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -137,33 +165,33 @@ func TestUpdateCluster(t *testing.T) { DBs: cluster.DBs{}, Proxy: &cluster.Proxy{}, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Initializing, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -172,20 +200,21 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", - RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.DefaultMaxStandbys * 2, - AdditionalWalSenders: cluster.DefaultAdditionalWalSenders * 2, - SynchronousReplication: false, - UsePgrewind: true, - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.DBInitModeNew, - Role: common.RoleMaster, + KeeperUID: k1, + RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, + MaxStandbys: cluster.DefaultMaxStandbys * 2, + AdditionalWalSenders: cluster.DefaultAdditionalWalSenders * 2, + SynchronousReplication: false, + UsePgrewind: true, + // revive:disable-next-line + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: cluster.NewDB, + Role: common.RolePrimary, Followers: []string{}, IncludeConfig: true, SynchronousStandbys: nil, @@ -198,39 +227,39 @@ func TestUpdateCluster(t *testing.T) { }, // #2 cluster initialization, more than one keeper, the first will be chosen to be the new master. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, + CurrentGeneration: g1, + Phase: cluster.Initializing, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -241,40 +270,40 @@ func TestUpdateCluster(t *testing.T) { DBs: cluster.DBs{}, Proxy: &cluster.Proxy{}, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - SynchronousReplication: cluster.BoolP(true), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + SynchronousReplication: util.ToPtr(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Initializing, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -283,20 +312,23 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", - RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.DefaultMaxStandbys * 2, - AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - SynchronousReplication: false, - UsePgrewind: true, - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - InitMode: cluster.DBInitModeNew, - Role: common.RoleMaster, + KeeperUID: k1, + RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, + MaxStandbys: cluster.DefaultMaxStandbys * 2, + AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, + SynchronousReplication: false, + UsePgrewind: true, + PGParameters: cluster.PGParameters{ + p1: v1, + p2: v2, + }, + InitMode: cluster.NewDB, + Role: common.RolePrimary, Followers: []string{}, IncludeConfig: true, SynchronousStandbys: nil, @@ -309,35 +341,35 @@ func TestUpdateCluster(t *testing.T) { }, // #3 cluster initialization, keeper initialization failed { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Initializing, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -346,15 +378,15 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", - InitMode: cluster.DBInitModeNew, + KeeperUID: k1, + InitMode: cluster.NewDB, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -366,34 +398,34 @@ func TestUpdateCluster(t *testing.T) { }, Proxy: &cluster.Proxy{}, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), - MergePgParameters: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + InitMode: &newCluster, + MergePgParameters: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseInitializing, + CurrentGeneration: g1, + Phase: cluster.Initializing, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -409,33 +441,33 @@ func TestUpdateCluster(t *testing.T) { // Normal phase // #4 One master and one standby, both healthy: no change from previous cd { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -444,88 +476,88 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -534,57 +566,57 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -592,33 +624,33 @@ func TestUpdateCluster(t *testing.T) { }, // #5 One master and one standby, master db not healthy: standby elected as new master. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -627,86 +659,86 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -715,49 +747,49 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -767,33 +799,33 @@ func TestUpdateCluster(t *testing.T) { }, // #6 From the previous test, new master (db2) converged. Old master setup to follow new master (db2). { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -802,82 +834,82 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 2, + CurrentGeneration: g2, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -886,42 +918,42 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db2": &cluster.DB{ - UID: "db2", - Generation: 3, + d2: &cluster.DB{ + UID: d2, + Generation: g3, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - Role: common.RoleMaster, + Role: common.RolePrimary, SynchronousReplication: false, - Followers: []string{"db3"}, + Followers: []string{d3}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 2, + CurrentGeneration: g2, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeResync, + InitMode: cluster.ResyncDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db2", + DBUID: d2, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -933,43 +965,44 @@ func TestUpdateCluster(t *testing.T) { }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ - MasterDBUID: "db2", + MasterDBUID: d2, EnabledProxies: []string{}, }, }, }, }, - // #7 One master and one standby, master db not healthy, standby not converged (old clusterview): no standby elected as new master, clusterview not changed. + // #7 One master and one standby, master db not healthy, standby not converged + // (old clusterview): no standby elected as new master, clusterview not changed. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -978,41 +1011,41 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1024,40 +1057,40 @@ func TestUpdateCluster(t *testing.T) { }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1066,41 +1099,41 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1112,9 +1145,9 @@ func TestUpdateCluster(t *testing.T) { }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -1122,33 +1155,33 @@ func TestUpdateCluster(t *testing.T) { }, // #8 One master and one standby, master healthy but not converged: standby elected as new master. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1157,18 +1190,18 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, @@ -1177,66 +1210,66 @@ func TestUpdateCluster(t *testing.T) { CurrentGeneration: 0, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1245,17 +1278,17 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1265,29 +1298,29 @@ func TestUpdateCluster(t *testing.T) { CurrentGeneration: 0, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -1295,43 +1328,45 @@ func TestUpdateCluster(t *testing.T) { }, }, }, - // #9 One master and one standby, 3 keepers (one available). Standby ok. No new standby db on free keeper created. + // #9 One master and one standby, 3 keepers (one available). + // Standby ok. + // No new standby db on free keeper created. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1340,96 +1375,96 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1438,99 +1473,101 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, }, - // #10 One master and one standby, 3 keepers (one available). Standby failed to converge (keeper healthy). New standby db on free keeper created. + // #10 One master and one standby, 3 keepers (one available). + // Standby failed to converge (keeper healthy). + // New standby db on free keeper created. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1539,41 +1576,41 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1585,48 +1622,48 @@ func TestUpdateCluster(t *testing.T) { }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1635,41 +1672,41 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1679,22 +1716,22 @@ func TestUpdateCluster(t *testing.T) { CurrentGeneration: 0, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeResync, + InitMode: cluster.ResyncDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1706,51 +1743,52 @@ func TestUpdateCluster(t *testing.T) { }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, }, - // #11 From previous test. new standby db "db3" converged, old standby db removed since exceeds MaxStandbysPerSender. + // #11 From previous test. + // new standby db d3 converged, old standby db removed since exceeds MaxStandbysPerSender. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1759,41 +1797,41 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 2, + CurrentGeneration: g2, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, @@ -1803,75 +1841,75 @@ func TestUpdateCluster(t *testing.T) { CurrentGeneration: 0, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1880,90 +1918,91 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 3, + d1: &cluster.DB{ + UID: d1, + Generation: g3, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db3"}, + Role: common.RolePrimary, + Followers: []string{d3}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 2, + CurrentGeneration: g2, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, }, - // #12 One master and one standby, 2 keepers. Standby failed to converge (keeper healthy). No standby db created since there's no free keeper. + // #12 One master and one standby, 2 keepers. Standby failed to converge (keeper healthy). + // No standby db created since there's no free keeper. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -1972,88 +2011,88 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2062,91 +2101,92 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, }, - // #13 One master and one keeper without db assigned. keeper2 dead for more then DeadKeeperRemovalInterval: keeper2 removed. + // #13 One master and one keeper without db assigned. + // keeper2 dead for more then DeadKeeperRemovalInterval: keeper2 removed. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2155,55 +2195,55 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2212,72 +2252,73 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, }, - // #14 Changed clusterSpec parameters. RequestTimeout, MaxStandbys, UsePgrewind, PGParameters should bet updated in the DBSpecs. + // #14 Changed clusterSpec parameters. + // RequestTimeout, MaxStandbys, UsePgrewind, PGParameters should bet updated in the DBSpecs. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2286,98 +2327,98 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, UsePgrewind: false, PGParameters: nil, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, UsePgrewind: false, PGParameters: nil, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, RequestTimeout: &cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.Uint16P(cluster.DefaultMaxStandbys * 2), - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - AdditionalWalSenders: cluster.Uint16P(cluster.DefaultAdditionalWalSenders * 2), - SynchronousReplication: cluster.BoolP(true), - UsePgrewind: cluster.BoolP(true), - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, + MaxStandbys: util.ToPtr(cluster.DefaultMaxStandbys * 2), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + AdditionalWalSenders: util.ToPtr(uint16(cluster.DefaultAdditionalWalSenders) * 2), + SynchronousReplication: util.ToPtr(true), + UsePgrewind: util.ToPtr(true), + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2386,22 +2427,25 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", - RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, - MaxStandbys: cluster.DefaultMaxStandbys * 2, - AdditionalWalSenders: cluster.DefaultAdditionalWalSenders * 2, - InitMode: cluster.DBInitModeNone, - SynchronousReplication: true, - UsePgrewind: true, - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + KeeperUID: k1, + RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, + MaxStandbys: cluster.DefaultMaxStandbys * 2, + AdditionalWalSenders: cluster.DefaultAdditionalWalSenders * 2, + InitMode: cluster.NoDB, + SynchronousReplication: true, + UsePgrewind: true, + PGParameters: cluster.PGParameters{ + p1: v1, + p2: v2, + }, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ @@ -2410,38 +2454,38 @@ func TestUpdateCluster(t *testing.T) { SynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout * 2}, MaxStandbys: cluster.DefaultMaxStandbys * 2, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders * 2, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, UsePgrewind: true, - PGParameters: cluster.PGParameters{"param01": "value01", "param02": "value02"}, - Role: common.RoleStandby, + PGParameters: cluster.PGParameters{p1: v1, p2: v2}, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -2450,34 +2494,34 @@ func TestUpdateCluster(t *testing.T) { // #15 One master and one standby all healthy. Synchronous replication // enabled right now in the cluster spec. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2486,87 +2530,87 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2575,49 +2619,49 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{fakeStandbyName}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -2631,34 +2675,34 @@ func TestUpdateCluster(t *testing.T) { // dbSpec.SynchronousReplication is false yet. The new master will have // SynchronousReplication true and a fake sync stanby. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2667,87 +2711,87 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2756,49 +2800,49 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{fakeStandbyName}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -2811,34 +2855,34 @@ func TestUpdateCluster(t *testing.T) { // master db not healthy: standby elected as new master since it's in // the SynchronousStandbys. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2847,88 +2891,88 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -2937,50 +2981,50 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db1"}, + SynchronousStandbys: []string{d1}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -2993,34 +3037,34 @@ func TestUpdateCluster(t *testing.T) { // stanby db not healthy: standby kept inside synchronousStandbys since // there's not better standby to choose { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3029,88 +3073,88 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3119,56 +3163,56 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -3180,42 +3224,42 @@ func TestUpdateCluster(t *testing.T) { // sync standby db2 not healthy: the other standby db3 choosed as new // sync standby. Both will appear as SynchronousStandbys { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3224,123 +3268,123 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3349,83 +3393,83 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -3436,42 +3480,42 @@ func TestUpdateCluster(t *testing.T) { // reported (db2) and the required in the spec (db2, db3) but it's not // healty so no master could be elected. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3480,123 +3524,123 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3605,83 +3649,83 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -3690,42 +3734,42 @@ func TestUpdateCluster(t *testing.T) { // #21 From #19. master have not yet reported the new sync standbys as in sync (db3). // db2 will remain the unique real in sync db in db1.Status.SynchronousStandbys { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3734,124 +3778,124 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3860,84 +3904,84 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -3948,42 +3992,42 @@ func TestUpdateCluster(t *testing.T) { // db1.Status.SynchronousStandbys and also db1.Spec.SynchronousStandbys // will contain only db2 (db3 removed) { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -3992,124 +4036,124 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4118,84 +4162,84 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 3, + d1: &cluster.DB{ + UID: d1, + Generation: g3, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -4204,42 +4248,42 @@ func TestUpdateCluster(t *testing.T) { // #23 From #19. master have reported the new sync standbys as in sync (db2, db3). // db2 will be removed from synchronousStandbys { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4248,124 +4292,124 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2", "db3"}, - CurSynchronousStandbys: []string{"db3"}, + SynchronousStandbys: []string{d2, d3}, + CurSynchronousStandbys: []string{d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4374,84 +4418,84 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 3, + d1: &cluster.DB{ + UID: d1, + Generation: g3, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 2, - SynchronousStandbys: []string{"db3"}, - CurSynchronousStandbys: []string{"db3"}, + SynchronousStandbys: []string{d3}, + CurSynchronousStandbys: []string{d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -4462,42 +4506,42 @@ func TestUpdateCluster(t *testing.T) { // reported (db2, db3) and the required in the spec (db3) so it'll be // elected as new master. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4506,123 +4550,123 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 3, + d1: &cluster.DB{ + UID: d1, + Generation: g3, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2", "db3"}, + SynchronousStandbys: []string{d2, d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db3", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d3, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4631,77 +4675,77 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 4, + d1: &cluster.DB{ + UID: d1, + Generation: g4, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db3"}, + SynchronousStandbys: []string{d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 2, - SynchronousStandbys: []string{"db2", "db3"}, + SynchronousStandbys: []string{d2, d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 2, + d3: &cluster.DB{ + UID: d3, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db1"}, + SynchronousStandbys: []string{d1}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -4714,42 +4758,42 @@ func TestUpdateCluster(t *testing.T) { // master (db1) and db2 failed, db3 elected as master. // This test checks that the db3 synchronousStandbys are correctly sorted { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4758,123 +4802,123 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2", "db3"}, - SynchronousStandbys: []string{"db2", "db3"}, + Role: common.RolePrimary, + Followers: []string{d2, d3}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2", "db3"}, + SynchronousStandbys: []string{d2, d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 1, + d3: &cluster.DB{ + UID: d3, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db3", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d3, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper3": &cluster.Keeper{ - UID: "keeper3", + k3: &cluster.Keeper{ + UID: k3, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -4883,77 +4927,77 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db2", "db3"}, + SynchronousStandbys: []string{d2, d3}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2", "db3"}, + SynchronousStandbys: []string{d2, d3}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db3": &cluster.DB{ - UID: "db3", - Generation: 2, + d3: &cluster.DB{ + UID: d3, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper3", + KeeperUID: k3, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db1", "db2"}, + SynchronousStandbys: []string{d1, d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -4966,56 +5010,56 @@ func TestUpdateCluster(t *testing.T) { // master (db1) and async (db2) with --never-synchronous-replica. // db2 is never elected as new sync. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeSynchronousReplica: cluster.BoolP(false), + CanBeSynchronousReplica: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{"stolonfakestandby"}, }, @@ -5026,90 +5070,90 @@ func TestUpdateCluster(t *testing.T) { CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeSynchronousReplica: cluster.BoolP(false), + CanBeSynchronousReplica: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{"stolonfakestandby"}, }, @@ -5120,36 +5164,36 @@ func TestUpdateCluster(t *testing.T) { CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -5160,190 +5204,190 @@ func TestUpdateCluster(t *testing.T) { // master (db1) and sync (db2) with --never-master. // db2 is never promoted as new master. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeMaster: cluster.BoolP(false), + CanBeMaster: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, - CurSynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, + CurSynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeMaster: cluster.BoolP(false), + CanBeMaster: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, - CurSynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, + CurSynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -5355,35 +5399,35 @@ func TestUpdateCluster(t *testing.T) { // dbSpec.SynchronousReplication is false yet. The new master will have // SynchronousReplication true and NO fake sync standby. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5392,88 +5436,88 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5482,49 +5526,49 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -5537,35 +5581,35 @@ func TestUpdateCluster(t *testing.T) { // master db not healthy: standby elected as new master since it's in // the SynchronousStandbys. No fake replica is added. { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5574,89 +5618,89 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db2", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d2, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5665,50 +5709,50 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: false, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 2, + d2: &cluster.DB{ + UID: d2, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{}, - SynchronousStandbys: []string{"db1"}, + SynchronousStandbys: []string{d1}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 2, + Generation: g2, Spec: cluster.ProxySpec{ MasterDBUID: "", EnabledProxies: []string{}, @@ -5721,35 +5765,35 @@ func TestUpdateCluster(t *testing.T) { // standby db not healthy: standby removed from synchronousStandbys even though // there's not better standby to choose { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5758,89 +5802,89 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, - SynchronousStandbys: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, + SynchronousStandbys: []string{d2}, ExternalSynchronousStandbys: []string{}, }, Status: cluster.DBStatus{ Healthy: true, CurrentGeneration: 1, - SynchronousStandbys: []string{"db2"}, + SynchronousStandbys: []string{d2}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, @@ -5849,18 +5893,18 @@ func TestUpdateCluster(t *testing.T) { }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{}, }, @@ -5870,35 +5914,35 @@ func TestUpdateCluster(t *testing.T) { SynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: false, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -5910,57 +5954,57 @@ func TestUpdateCluster(t *testing.T) { // StrictSyncRepl is set to false. Db2 is never elected as new sync, and fake replica // is removed from db1 { - cd: &cluster.ClusterData{ + cd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeSynchronousReplica: cluster.BoolP(false), + CanBeSynchronousReplica: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 1, + d1: &cluster.DB{ + UID: d1, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{"stolonfakestandby"}, }, @@ -5971,91 +6015,91 @@ func TestUpdateCluster(t *testing.T) { CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, }, - outcd: &cluster.ClusterData{ + outcd: &cluster.Data{ Cluster: &cluster.Cluster{ - UID: "cluster1", - Generation: 1, - Spec: &cluster.ClusterSpec{ + UID: c1, + Generation: g1, + Spec: &cluster.Spec{ ConvergenceTimeout: &cluster.Duration{Duration: cluster.DefaultConvergenceTimeout}, InitTimeout: &cluster.Duration{Duration: cluster.DefaultInitTimeout}, SyncTimeout: &cluster.Duration{Duration: cluster.DefaultSyncTimeout}, - MaxStandbysPerSender: cluster.Uint16P(cluster.DefaultMaxStandbysPerSender), - SynchronousReplication: cluster.BoolP(true), - MinSynchronousStandbys: cluster.Uint16P(0), + MaxStandbysPerSender: util.ToPtr(cluster.DefaultMaxStandbysPerSender), + SynchronousReplication: util.ToPtr(true), + MinSynchronousStandbys: util.ToPtr(uint16(0)), }, Status: cluster.ClusterStatus{ - CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, - Master: "db1", + CurrentGeneration: g1, + Phase: cluster.Normal, + Master: d1, }, }, Keepers: cluster.Keepers{ - "keeper1": &cluster.Keeper{ - UID: "keeper1", + k1: &cluster.Keeper{ + UID: k1, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, }, }, - "keeper2": &cluster.Keeper{ - UID: "keeper2", + k2: &cluster.Keeper{ + UID: k2, Spec: &cluster.KeeperSpec{}, Status: cluster.KeeperStatus{ Healthy: true, LastHealthyTime: now, - CanBeSynchronousReplica: cluster.BoolP(false), + CanBeSynchronousReplica: util.ToPtr(false), }, }, }, DBs: cluster.DBs{ - "db1": &cluster.DB{ - UID: "db1", - Generation: 2, + d1: &cluster.DB{ + UID: d1, + Generation: g2, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper1", + KeeperUID: k1, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: true, - Role: common.RoleMaster, - Followers: []string{"db2"}, + Role: common.RolePrimary, + Followers: []string{d2}, SynchronousStandbys: []string{}, ExternalSynchronousStandbys: []string{}, }, @@ -6066,36 +6110,36 @@ func TestUpdateCluster(t *testing.T) { CurSynchronousStandbys: []string{}, }, }, - "db2": &cluster.DB{ - UID: "db2", - Generation: 1, + d2: &cluster.DB{ + UID: d2, + Generation: g1, ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ - KeeperUID: "keeper2", + KeeperUID: k2, RequestTimeout: cluster.Duration{Duration: cluster.DefaultRequestTimeout}, MaxStandbys: cluster.DefaultMaxStandbys, AdditionalWalSenders: cluster.DefaultAdditionalWalSenders, - InitMode: cluster.DBInitModeNone, + InitMode: cluster.NoDB, SynchronousReplication: false, - Role: common.RoleStandby, + Role: common.RoleReplica, Followers: []string{}, FollowConfig: &cluster.FollowConfig{ Type: cluster.FollowTypeInternal, - DBUID: "db1", + DBUID: d1, }, SynchronousStandbys: nil, ExternalSynchronousStandbys: nil, }, Status: cluster.DBStatus{ Healthy: true, - CurrentGeneration: 1, + CurrentGeneration: g1, }, }, }, Proxy: &cluster.Proxy{ - Generation: 1, + Generation: g1, Spec: cluster.ProxySpec{ - MasterDBUID: "db1", + MasterDBUID: d1, EnabledProxies: []string{}, }, }, @@ -6104,7 +6148,8 @@ func TestUpdateCluster(t *testing.T) { } for i, tt := range tests { - s := &Sentinel{uid: "sentinel01", UIDFn: testUIDFn, RandFn: testRandFn, dbConvergenceInfos: make(map[string]*DBConvergenceInfo)} + s := &Sentinel{uid: "sentinel01", UIDFn: testUIDFn, RandFn: testRandFn, + dbConvergenceInfos: map[string]*DBConvergenceInfo{}} // reset curUID func value to latest db uid curUID = 0 @@ -6115,7 +6160,8 @@ func TestUpdateCluster(t *testing.T) { } } - // Populate db convergence timers, these are populated with a negative timer to make them result like not converged. + // Populate db convergence timers, these are populated with a negative + // timer to make them result like not converged. for _, db := range tt.cd.DBs { s.dbConvergenceInfos[db.UID] = &DBConvergenceInfo{Generation: 0, Timer: int64(-1000 * time.Hour)} } @@ -6123,7 +6169,7 @@ func TestUpdateCluster(t *testing.T) { fmt.Printf("test #%d\n", i) t.Logf("test #%d", i) - outcd, err := s.updateCluster(tt.cd, cluster.ProxiesInfo{}) + outcd, err := s.updateCluster(ctx, tt.cd, cluster.ProxiesInfo{}) if tt.err != nil { if err == nil { t.Errorf("got no error, wanted error: %v", tt.err) @@ -6142,10 +6188,19 @@ func TestUpdateCluster(t *testing.T) { } func TestActiveProxiesInfos(t *testing.T) { - proxyInfo1 := cluster.ProxyInfo{UID: "proxy1", InfoUID: "infoUID1", ProxyTimeout: cluster.DefaultProxyTimeout} - proxyInfo2 := cluster.ProxyInfo{UID: "proxy2", InfoUID: "infoUID2", ProxyTimeout: cluster.DefaultProxyTimeout} - proxyInfoWithDifferentInfoUID := cluster.ProxyInfo{UID: "proxy2", InfoUID: "differentInfoUID"} - var secToNanoSecondMultiplier int64 = 1000000000 + const ( + pr1 = "proxy01" + pr2 = "proxy02" + ) + proxyInfo1 := cluster.ProxyInfo{UID: pr1, InfoUID: "infoUID1", ProxyTimeout: cluster.DefaultProxyTimeout} + proxyInfo2 := cluster.ProxyInfo{UID: pr2, InfoUID: "infoUID2", ProxyTimeout: cluster.DefaultProxyTimeout} + proxyInfoWithDifferentInfoUID := cluster.ProxyInfo{UID: pr2, InfoUID: "differentInfoUID"} + var ( + secToNanoSecondMultiplier int64 = 1000000000 + + td15ns = 15 * secToNanoSecondMultiplier + td45ns = 45 * secToNanoSecondMultiplier + ) tests := []struct { name string proxyInfoHistories ProxyInfoHistories @@ -6161,50 +6216,76 @@ func TestActiveProxiesInfos(t *testing.T) { expectedProxyInfoHistories: nil, }, { - name: "should append to histories when called with proxyInfos", - proxyInfoHistories: make(ProxyInfoHistories), - proxiesInfos: cluster.ProxiesInfo{"proxy1": &proxyInfo1, "proxy2": &proxyInfo2}, - expectedActiveProxies: cluster.ProxiesInfo{"proxy1": &proxyInfo1, "proxy2": &proxyInfo2}, - expectedProxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2}}, + name: "should append to histories when called with proxyInfos", + proxyInfoHistories: make(ProxyInfoHistories), + proxiesInfos: cluster.ProxiesInfo{pr1: &proxyInfo1, pr2: &proxyInfo2}, + expectedActiveProxies: cluster.ProxiesInfo{pr1: &proxyInfo1, pr2: &proxyInfo2}, + expectedProxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2}, + }, }, { - name: "should update to histories if infoUID is different", - proxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now()}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now()}}, - proxiesInfos: cluster.ProxiesInfo{"proxy1": &proxyInfo1, "proxy2": &proxyInfoWithDifferentInfoUID}, - expectedActiveProxies: cluster.ProxiesInfo{"proxy1": &proxyInfo1, "proxy2": &proxyInfoWithDifferentInfoUID}, - expectedProxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfoWithDifferentInfoUID}}, + name: "should update to histories if infoUID is different", + proxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now()}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now()}, + }, + proxiesInfos: cluster.ProxiesInfo{pr1: &proxyInfo1, pr2: &proxyInfoWithDifferentInfoUID}, + expectedActiveProxies: cluster.ProxiesInfo{pr1: &proxyInfo1, pr2: &proxyInfoWithDifferentInfoUID}, + expectedProxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfoWithDifferentInfoUID}, + }, }, { - name: "should remove from active proxies if is not updated for twice the DefaultProxyTimeout", - proxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now() - (3 * 15 * secToNanoSecondMultiplier)}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now() - (1 * 15 * secToNanoSecondMultiplier)}}, - proxiesInfos: cluster.ProxiesInfo{"proxy1": &proxyInfo1, "proxy2": &proxyInfo2}, - expectedActiveProxies: cluster.ProxiesInfo{"proxy2": &proxyInfo2}, - expectedProxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2}}, + name: "should remove from active proxies if is not updated for twice the DefaultProxyTimeout", + proxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now() - (td45ns)}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now() - (td15ns)}, + }, + proxiesInfos: cluster.ProxiesInfo{pr1: &proxyInfo1, pr2: &proxyInfo2}, + expectedActiveProxies: cluster.ProxiesInfo{pr2: &proxyInfo2}, + expectedProxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2}, + }, }, { - name: "should remove proxy from sentinel's local history if the proxy is removed in store", - proxyInfoHistories: ProxyInfoHistories{"proxy1": &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now()}, "proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now()}}, - proxiesInfos: cluster.ProxiesInfo{"proxy2": &proxyInfo2}, - expectedActiveProxies: cluster.ProxiesInfo{"proxy2": &proxyInfo2}, - expectedProxyInfoHistories: ProxyInfoHistories{"proxy2": &ProxyInfoHistory{ProxyInfo: &proxyInfo2}}, + name: "should remove proxy from sentinel's local history if the proxy is removed in store", + proxyInfoHistories: ProxyInfoHistories{ + pr1: &ProxyInfoHistory{ProxyInfo: &proxyInfo1, Timer: timer.Now()}, + pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2, Timer: timer.Now()}, + }, + proxiesInfos: cluster.ProxiesInfo{pr2: &proxyInfo2}, + expectedActiveProxies: cluster.ProxiesInfo{pr2: &proxyInfo2}, + expectedProxyInfoHistories: ProxyInfoHistories{pr2: &ProxyInfoHistory{ProxyInfo: &proxyInfo2}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - s := &Sentinel{uid: "sentinel01", UIDFn: testUIDFn, RandFn: testRandFn, dbConvergenceInfos: make(map[string]*DBConvergenceInfo), proxyInfoHistories: test.proxyInfoHistories} + s := &Sentinel{ + uid: "sentinel01", + UIDFn: testUIDFn, + RandFn: testRandFn, + dbConvergenceInfos: map[string]*DBConvergenceInfo{}, + proxyInfoHistories: test.proxyInfoHistories, + } actualActiveProxies := s.activeProxiesInfos(test.proxiesInfos) if !reflect.DeepEqual(actualActiveProxies, test.expectedActiveProxies) { t.Errorf("Expected proxiesInfos to be %v but got %v", test.expectedActiveProxies, actualActiveProxies) } if !isProxyInfoHistoriesEqual(s.proxyInfoHistories, test.expectedProxyInfoHistories) { - t.Errorf("Expected proxyInfoHistories to be %v but got %v", test.expectedProxyInfoHistories, s.proxyInfoHistories) + t.Errorf("Expected proxyInfoHistories to be %v but got %v", + test.expectedProxyInfoHistories, s.proxyInfoHistories) } }) } } -func isProxyInfoHistoriesEqual(actualProxyInfoHistories ProxyInfoHistories, expectedProxyInfoHistories ProxyInfoHistories) bool { +func isProxyInfoHistoriesEqual(actualProxyInfoHistories ProxyInfoHistories, + expectedProxyInfoHistories ProxyInfoHistories) bool { if len(actualProxyInfoHistories) != len(expectedProxyInfoHistories) { return false } @@ -6226,13 +6307,13 @@ func testUIDFn() string { return fmt.Sprintf("%s%d", "db", curUID) } -func testRandFn(i int) int { +func testRandFn(_ int) int { return 0 } -func testEqualCD(cd1, cd2 *cluster.ClusterData) bool { +func testEqualCD(cd1, cd2 *cluster.Data) bool { // ignore times - for _, cd := range []*cluster.ClusterData{cd1, cd2} { + for _, cd := range []*cluster.Data{cd1, cd2} { cd.Cluster.ChangeTime = time.Time{} for _, k := range cd.Keepers { k.ChangeTime = time.Time{} @@ -6243,5 +6324,4 @@ func testEqualCD(cd1, cd2 *cluster.ClusterData) bool { cd.Proxy.ChangeTime = time.Time{} } return reflect.DeepEqual(cd1, cd2) - } diff --git a/cmd/sentinel/main.go b/cmd/sentinel/main.go index c6203a06b..7ce01f888 100644 --- a/cmd/sentinel/main.go +++ b/cmd/sentinel/main.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package main is a package that provides functionality concerning postgresSQL lifecycles package main import ( - "github.com/sorintlab/stolon/cmd/sentinel/cmd" + "github.com/pgvillage-tools/stolon/cmd/sentinel/cmd" ) func main() { diff --git a/cmd/stolonctl/cmd/clusterdata.go b/cmd/stolonctl/cmd/clusterdata.go index d8e44405d..a602a563e 100644 --- a/cmd/stolonctl/cmd/clusterdata.go +++ b/cmd/stolonctl/cmd/clusterdata.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,19 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package cmd is a package which provides utilities that underly the specific command package cmd import ( "context" "encoding/json" + "errors" "fmt" "io" - "io/ioutil" "os" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/logging" + ststore "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" ) @@ -60,18 +63,37 @@ var cmdWriteClusterData = &cobra.Command{ } func init() { - cmdReadClusterData.PersistentFlags().BoolVar(&readClusterdataOpts.pretty, "pretty", false, "pretty print") + cmdReadClusterData.PersistentFlags().BoolVar( + &readClusterdataOpts.pretty, + "pretty", + false, + "pretty print", + ) cmdClusterData.AddCommand(cmdReadClusterData) - cmdWriteClusterData.PersistentFlags().StringVarP(&writeClusterdataOpts.file, "file", "f", "", "file containing the new cluster data") - cmdWriteClusterData.PersistentFlags().BoolVarP(&writeClusterdataOpts.forceYes, "yes", "y", false, "don't ask for confirmation") + cmdWriteClusterData.PersistentFlags().StringVarP( + &writeClusterdataOpts.file, + "file", + "f", + "", + "file containing the new cluster data", + ) + cmdWriteClusterData.PersistentFlags().BoolVarP( + &writeClusterdataOpts.forceYes, + "yes", + "y", + false, + "don't ask for confirmation", + ) cmdClusterData.AddCommand(cmdWriteClusterData) CmdStolonCtl.AddCommand(cmdClusterData) } -func readClusterdata(cmd *cobra.Command, args []string) { - e, err := cmdcommon.NewStore(&cfg.CommonConfig) +func readClusterdata(_ *cobra.Command, _ []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } @@ -98,27 +120,26 @@ func readClusterdata(cmd *cobra.Command, args []string) { stdout("%s", clusterdataj) } -func isSafeToWriteClusterData(store store.Store) error { +func isSafeToWriteClusterData(store ststore.Store) error { if cd, _, err := store.GetClusterData(context.TODO()); err != nil { return err } else if cd != nil { if !writeClusterdataOpts.forceYes { - return fmt.Errorf("WARNING: cluster data already available use --yes to override") - } else { - stdout("WARNING: The current cluster data will be removed") + return errors.New("WARNING: cluster data already available use --yes to override") } + stdout("WARNING: The current cluster data will be removed") } return nil } -func clusterData(data []byte) (*cluster.ClusterData, error) { - cd := cluster.ClusterData{} +func clusterData(data []byte) (*cluster.Data, error) { + cd := cluster.Data{} err := json.Unmarshal(data, &cd) return &cd, err } -func writeClusterdata(reader io.Reader, s store.Store) error { - data, err := ioutil.ReadAll(reader) +func writeClusterdata(reader io.Reader, s ststore.Store) error { + data, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("error while reading data: %v", err) } @@ -143,6 +164,9 @@ func writeClusterdata(reader io.Reader, s store.Store) error { } func runWriteClusterdata(_ *cobra.Command, _ []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) var reader io.Reader if writeClusterdataOpts.file == "" || writeClusterdataOpts.file == "-" { reader = os.Stdin @@ -151,10 +175,12 @@ func runWriteClusterdata(_ *cobra.Command, _ []string) { if err != nil { die("cannot read file: %v", err) } - defer file.Close() + if err := file.Close(); err != nil { + logger.Fatal().AnErr("err", err).Msg("closing file failed") + } reader = file } - s, err := cmdcommon.NewStore(&cfg.CommonConfig) + s, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("failed to create new store %v", err) } diff --git a/cmd/stolonctl/cmd/clusterdata_test.go b/cmd/stolonctl/cmd/clusterdata_test.go index 9c87278cc..3daee172b 100644 --- a/cmd/stolonctl/cmd/clusterdata_test.go +++ b/cmd/stolonctl/cmd/clusterdata_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +16,13 @@ package cmd import ( - "fmt" + "errors" "strings" "testing" "github.com/golang/mock/gomock" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/mock/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + mockstore "github.com/pgvillage-tools/stolon/internal/mock/store" ) func TestWriteClusterdata(t *testing.T) { @@ -32,7 +33,7 @@ func TestWriteClusterdata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - store := mock_store.NewMockStore(ctrl) + store := mockstore.NewMockStore(ctrl) reader := strings.Reader{} err := writeClusterdata(&reader, store) @@ -52,7 +53,7 @@ func TestWriteClusterdata(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - store := mock_store.NewMockStore(ctrl) + store := mockstore.NewMockStore(ctrl) reader := strings.NewReader("{a}") err := writeClusterdata(reader, store) @@ -73,9 +74,9 @@ func TestWriteClusterdata(t *testing.T) { defer ctrl.Finish() reader := strings.NewReader("{}") - store := mock_store.NewMockStore(ctrl) + store := mockstore.NewMockStore(ctrl) - store.EXPECT().GetClusterData(gomock.Any()).Return(nil, nil, fmt.Errorf("Error in getting cluster data")) + store.EXPECT().GetClusterData(gomock.Any()).Return(nil, nil, errors.New("Error in getting cluster data")) err := writeClusterdata(reader, store) @@ -96,8 +97,8 @@ func TestWriteClusterdata(t *testing.T) { defer ctrl.Finish() reader := strings.NewReader("{}") - store := mock_store.NewMockStore(ctrl) - store.EXPECT().GetClusterData(gomock.Any()).Return(&cluster.ClusterData{}, nil, nil) + store := mockstore.NewMockStore(ctrl) + store.EXPECT().GetClusterData(gomock.Any()).Return(&cluster.Data{}, nil, nil) err := writeClusterdata(reader, store) @@ -119,10 +120,10 @@ func TestWriteClusterdata(t *testing.T) { defer ctrl.Finish() reader := strings.NewReader("{}") - store := mock_store.NewMockStore(ctrl) - cd := &cluster.ClusterData{} + store := mockstore.NewMockStore(ctrl) + cd := &cluster.Data{} store.EXPECT().GetClusterData(gomock.Any()).Return(cd, nil, nil) - store.EXPECT().PutClusterData(gomock.Any(), cd).Return(fmt.Errorf("error while uploading the cluster data")) + store.EXPECT().PutClusterData(gomock.Any(), cd).Return(errors.New("error while uploading the cluster data")) err := writeClusterdata(reader, store) @@ -144,8 +145,8 @@ func TestWriteClusterdata(t *testing.T) { defer ctrl.Finish() reader := strings.NewReader("{}") - store := mock_store.NewMockStore(ctrl) - cd := &cluster.ClusterData{} + store := mockstore.NewMockStore(ctrl) + cd := &cluster.Data{} store.EXPECT().GetClusterData(gomock.Any()).Return(cd, nil, nil) store.EXPECT().PutClusterData(gomock.Any(), cd).Return(nil) @@ -155,5 +156,4 @@ func TestWriteClusterdata(t *testing.T) { t.Error("expected not to have an error") } }) - } diff --git a/cmd/stolonctl/cmd/failkeeper.go b/cmd/stolonctl/cmd/failkeeper.go index 58e86f384..b0e5700c7 100644 --- a/cmd/stolonctl/cmd/failkeeper.go +++ b/cmd/stolonctl/cmd/failkeeper.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +18,12 @@ package cmd import ( "context" - cmdcommon "github.com/sorintlab/stolon/cmd" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" "github.com/spf13/cobra" ) +// revive:disable + var failKeeperCmd = &cobra.Command{ Use: "failkeeper [keeper uid]", Short: `Force keeper as "temporarily" failed. The sentinel will compute a new clusterdata considering it as failed and then restore its state to the real one.`, @@ -28,11 +31,15 @@ var failKeeperCmd = &cobra.Command{ Run: failKeeper, } +//revive:enable + func init() { CmdStolonCtl.AddCommand(failKeeperCmd) } -func failKeeper(cmd *cobra.Command, args []string) { +func failKeeper(_ *cobra.Command, args []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() if len(args) > 1 { die("too many arguments") } @@ -43,7 +50,7 @@ func failKeeper(cmd *cobra.Command, args []string) { keeperID := args[0] - store, err := cmdcommon.NewStore(&cfg.CommonConfig) + store, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } diff --git a/cmd/stolonctl/cmd/init.go b/cmd/stolonctl/cmd/init.go index cba411a8a..bfc40b939 100644 --- a/cmd/stolonctl/cmd/init.go +++ b/cmd/stolonctl/cmd/init.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,12 +18,12 @@ package cmd import ( "context" "encoding/json" - "io/ioutil" + "io" "os" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/common" "github.com/spf13/cobra" ) @@ -32,6 +33,7 @@ var cmdInit = &cobra.Command{ Short: "Initialize a new cluster", } +// InitOptions is a struct which can contain initiation options type InitOptions struct { file string forceYes bool @@ -46,7 +48,9 @@ func init() { CmdStolonCtl.AddCommand(cmdInit) } -func initCluster(cmd *cobra.Command, args []string) { +func initCluster(_ *cobra.Command, args []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() if len(args) > 1 { die("too many arguments") } @@ -62,12 +66,12 @@ func initCluster(cmd *cobra.Command, args []string) { dataSupplied = true var err error if initOpts.file == "-" { - data, err = ioutil.ReadAll(os.Stdin) + data, err = io.ReadAll(os.Stdin) if err != nil { die("cannot read from stdin: %v", err) } } else { - data, err = ioutil.ReadFile(initOpts.file) + data, err = os.ReadFile(initOpts.file) if err != nil { die("cannot read file: %v", err) } @@ -75,7 +79,7 @@ func initCluster(cmd *cobra.Command, args []string) { } } - e, err := cmdcommon.NewStore(&cfg.CommonConfig) + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } @@ -106,15 +110,16 @@ func initCluster(cmd *cobra.Command, args []string) { die("cannot get cluster data: %v", err) } - var cs *cluster.ClusterSpec + var cs *cluster.Spec if dataSupplied { if err := json.Unmarshal(data, &cs); err != nil { die("failed to unmarshal cluster spec: %v", err) } } else { // Define a new cluster spec with initMode "new" - cs = &cluster.ClusterSpec{} - cs.InitMode = cluster.ClusterInitModeP(cluster.ClusterInitModeNew) + cs = &cluster.Spec{} + newCluster := cluster.New + cs.InitMode = &newCluster } if err := cs.Validate(); err != nil { diff --git a/cmd/stolonctl/cmd/internal/mock/register/discovery.go b/cmd/stolonctl/cmd/internal/mock/register/discovery.go index 31db9161a..6881b0ca7 100644 --- a/cmd/stolonctl/cmd/internal/mock/register/discovery.go +++ b/cmd/stolonctl/cmd/internal/mock/register/discovery.go @@ -7,7 +7,7 @@ package mock_register import ( gomock "github.com/golang/mock/gomock" api "github.com/hashicorp/consul/api" - register "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" + register "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/register" reflect "reflect" ) @@ -42,7 +42,7 @@ func (m *MockServiceDiscovery) Register(info *register.ServiceInfo) error { } // Register indicates an expected call of Register -func (mr *MockServiceDiscoveryMockRecorder) Register(info interface{}) *gomock.Call { +func (mr *MockServiceDiscoveryMockRecorder) Register(info any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockServiceDiscovery)(nil).Register), info) } @@ -55,7 +55,7 @@ func (m *MockServiceDiscovery) Services(name string) (register.ServiceInfos, err } // Services indicates an expected call of Services -func (mr *MockServiceDiscoveryMockRecorder) Services(name interface{}) *gomock.Call { +func (mr *MockServiceDiscoveryMockRecorder) Services(name any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Services", reflect.TypeOf((*MockServiceDiscovery)(nil).Services), name) } @@ -67,7 +67,7 @@ func (m *MockServiceDiscovery) DeRegister(info *register.ServiceInfo) error { } // DeRegister indicates an expected call of DeRegister -func (mr *MockServiceDiscoveryMockRecorder) DeRegister(info interface{}) *gomock.Call { +func (mr *MockServiceDiscoveryMockRecorder) DeRegister(info any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeRegister", reflect.TypeOf((*MockServiceDiscovery)(nil).DeRegister), info) } @@ -102,7 +102,7 @@ func (m *MockConsulAgent) ServiceRegister(service *api.AgentServiceRegistration) } // ServiceRegister indicates an expected call of ServiceRegister -func (mr *MockConsulAgentMockRecorder) ServiceRegister(service interface{}) *gomock.Call { +func (mr *MockConsulAgentMockRecorder) ServiceRegister(service any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceRegister", reflect.TypeOf((*MockConsulAgent)(nil).ServiceRegister), service) } @@ -114,7 +114,7 @@ func (m *MockConsulAgent) ServiceDeregister(serviceID string) error { } // ServiceDeregister indicates an expected call of ServiceDeregister -func (mr *MockConsulAgentMockRecorder) ServiceDeregister(serviceID interface{}) *gomock.Call { +func (mr *MockConsulAgentMockRecorder) ServiceDeregister(serviceID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceDeregister", reflect.TypeOf((*MockConsulAgent)(nil).ServiceDeregister), serviceID) } @@ -164,6 +164,6 @@ func (m *MockConsulCatalog) Service(service, tag string, q *api.QueryOptions) ([ } // Service indicates an expected call of Service -func (mr *MockConsulCatalogMockRecorder) Service(service, tag, q interface{}) *gomock.Call { +func (mr *MockConsulCatalogMockRecorder) Service(service, tag, q any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Service", reflect.TypeOf((*MockConsulCatalog)(nil).Service), service, tag, q) } diff --git a/cmd/stolonctl/cmd/promote.go b/cmd/stolonctl/cmd/promote.go index 8f900957e..346a85787 100644 --- a/cmd/stolonctl/cmd/promote.go +++ b/cmd/stolonctl/cmd/promote.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +19,9 @@ import ( "context" "os" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" ) @@ -37,12 +38,14 @@ func init() { CmdStolonCtl.AddCommand(cmdPromote) } -func promote(cmd *cobra.Command, args []string) { +func promote(_ *cobra.Command, args []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() if len(args) > 0 { die("too many arguments") } - e, err := cmdcommon.NewStore(&cfg.CommonConfig) + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } @@ -73,11 +76,12 @@ func promote(cmd *cobra.Command, args []string) { } ds := cd.Cluster.DefSpec() - if *ds.Role == cluster.ClusterRoleMaster { + if *ds.Role == cluster.Primary { stderr("cluster spec role already set to master") os.Exit(0) } - cd.Cluster.Spec.Role = cluster.ClusterRoleP(cluster.ClusterRoleMaster) + primaryRole := cluster.Primary + cd.Cluster.Spec.Role = &primaryRole if err = cd.Cluster.UpdateSpec(cd.Cluster.Spec); err != nil { die("Cannot update cluster spec: %v", err) diff --git a/cmd/stolonctl/cmd/register.go b/cmd/stolonctl/cmd/register.go index ec2b99b8d..e6dc37fe8 100644 --- a/cmd/stolonctl/cmd/register.go +++ b/cmd/stolonctl/cmd/register.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,21 +16,21 @@ package cmd import ( + "context" "fmt" "os" "os/signal" "syscall" "time" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" - slog "github.com/sorintlab/stolon/internal/log" - "github.com/sorintlab/stolon/internal/store" + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/register" + "github.com/pgvillage-tools/stolon/internal/logging" + stolonstore "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" - "go.uber.org/zap" ) -//Register command to register stolon master and slave for service discovery +// Register command to register stolon master and slave for service discovery var Register = &cobra.Command{ Use: "register", Short: "Register stolon keepers for service discovery", @@ -38,20 +39,75 @@ var Register = &cobra.Command{ } var rCfg register.Config -var log = slog.S() + +const sleepinterval = 10 func init() { - Register.PersistentFlags().StringVar(&rCfg.Backend, "register-backend", "consul", "register backend type (consul)") - Register.PersistentFlags().StringVar(&rCfg.Endpoints, "register-endpoints", "http://127.0.0.1:8500", "a comma-delimited list of register endpoints (use https scheme for tls communication) defaults: http://127.0.0.1:8500 for consul") - Register.PersistentFlags().StringVar(&rCfg.TLSCertFile, "register-cert-file", "", "certificate file for client identification to the register") - Register.PersistentFlags().StringVar(&rCfg.TLSKeyFile, "register-key", "", "private key file for client identification to the register") - Register.PersistentFlags().BoolVar(&rCfg.TLSInsecureSkipVerify, "register-skip-tls-verify", false, "skip register certificate verification (insecure!!!)") - Register.PersistentFlags().StringVar(&rCfg.TLSCAFile, "register-ca-file", "", "verify certificates of HTTPS-enabled register servers using this CA bundle") - Register.PersistentFlags().BoolVar(&rCfg.RegisterMaster, "register-master", false, "register master as well for service discovery (use it with caution!!!)") - Register.PersistentFlags().StringVar(&rCfg.TagMasterAs, "tag-master-as", "master", "a comma-delimited list of tag to be used when registering master") - Register.PersistentFlags().StringVar(&rCfg.TagSlaveAs, "tag-slave-as", "slave", "a comma-delimited list of tag to be used when registering slave") - Register.PersistentFlags().BoolVar(&cfg.Debug, "debug", false, "enable debug logging") - Register.PersistentFlags().IntVar(&rCfg.SleepInterval, "sleep-interval", 10, "number of seconds to sleep before probing for change") + Register.PersistentFlags().StringVar( + &rCfg.Backend, + "register-backend", + "consul", + "register backend type (consul)", + ) + Register.PersistentFlags().StringVar( + &rCfg.Endpoints, + "register-endpoints", + "http://127.0.0.1:8500", + //revive:disable-next-line + "a comma-delimited list of register endpoints (use https scheme for tls communication) defaults: http://127.0.0.1:8500 for consul") + Register.PersistentFlags().StringVar( + &rCfg.TLSCertFile, + "register-cert-file", + "", + "certificate file for client identification to the register", + ) + Register.PersistentFlags().StringVar( + &rCfg.TLSKeyFile, + "register-key", + "", + "private key file for client identification to the register", + ) + Register.PersistentFlags().BoolVar( + &rCfg.TLSInsecureSkipVerify, + "register-skip-tls-verify", + false, + "skip register certificate verification (insecure!!!)", + ) + Register.PersistentFlags().StringVar( + &rCfg.TLSCAFile, + "register-ca-file", + "", + "verify certificates of HTTPS-enabled register servers using this CA bundle", + ) + Register.PersistentFlags().BoolVar(&rCfg.RegisterMaster, + "register-master", + false, + "register master as well for service discovery (use it with caution!!!)", + ) + Register.PersistentFlags().StringVar( + &rCfg.TagMasterAs, + "tag-master-as", + "master", + "a comma-delimited list of tag to be used when registering master", + ) + Register.PersistentFlags().StringVar( + &rCfg.TagSlaveAs, + "tag-slave-as", + "slave", + "a comma-delimited list of tag to be used when registering slave", + ) + Register.PersistentFlags().BoolVar( + &cfg.Debug, + "dbug", + false, + "enable debug logging", + ) + Register.PersistentFlags().IntVar( + &rCfg.SleepInterval, + "sleep-interval", + sleepinterval, + "number of seconds to sleep before probing for change", + ) CmdStolonCtl.AddCommand(Register) } @@ -67,39 +123,30 @@ func checkConfig(cfg *config, rCfg *register.Config) error { } func runRegister(c *cobra.Command, _ []string) { - switch cfg.LogLevel { - case "error": - slog.SetLevel(zap.ErrorLevel) - case "warn": - slog.SetLevel(zap.WarnLevel) - case "info": - slog.SetLevel(zap.InfoLevel) - case "debug": - slog.SetLevel(zap.DebugLevel) - default: - die("invalid log level: %v", cfg.LogLevel) - } + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + logging.SetStaticLevel(cfg.LogLevel) if cfg.Debug { - slog.SetDebug() + logging.SetStaticLevel("debug") } if cmd.IsColorLoggerEnable(c, &cfg.CommonConfig) { - log = slog.SColor() + logging.EnableColor() } if err := checkConfig(&cfg, &rCfg); err != nil { - die(err.Error()) + die("%s", err.Error()) } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - if err := registerCluster(sigs, &cfg, &rCfg); err != nil { - die(err.Error()) + if err := registerCluster(ctx, sigs, &cfg, &rCfg); err != nil { + die("%s", err.Error()) } } -func registerCluster(sigs chan os.Signal, cfg *config, rCfg *register.Config) error { - s, err := cmd.NewStore(&cfg.CommonConfig) +func registerCluster(ctx context.Context, sigs chan os.Signal, cfg *config, rCfg *register.Config) error { + s, err := cmd.NewStore(ctx, &cfg.CommonConfig) if err != nil { return err } @@ -118,7 +165,7 @@ func registerCluster(sigs chan os.Signal, cfg *config, rCfg *register.Config) er return nil case <-timerCh: go func() { - checkAndRegisterMasterAndSlaves(cfg.ClusterName, s, service, rCfg.RegisterMaster) + checkAndRegisterMasterAndSlaves(ctx, cfg.ClusterName, s, service, rCfg.RegisterMaster) endCh <- struct{}{} }() case <-endCh: @@ -127,32 +174,44 @@ func registerCluster(sigs chan os.Signal, cfg *config, rCfg *register.Config) er } } -func checkAndRegisterMasterAndSlaves(clusterName string, store store.Store, discovery register.ServiceDiscovery, registerMaster bool) { +func checkAndRegisterMasterAndSlaves( + ctx context.Context, + clusterName string, + store stolonstore.Store, + discovery register.ServiceDiscovery, + registerMaster bool, +) { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) discoveredServices, err := discovery.Services(clusterName) if err != nil { - log.Errorf("unable to get info about existing services: %v", err) + logger.Error().AnErr("err", err).Msg("unable to get info about existing services") return } - existingServices, err := getExistingServices(clusterName, store, registerMaster) - if err == nil { - log.Debugf("found services %v", existingServices) - } else { - log.Errorf("%s skipping", err.Error()) + existingServices, err := getExistingServices(ctx, clusterName, store, registerMaster) + if err != nil { + logger.Error().AnErr("err", err).Msg("skipping") return } + logger.Debug().Any("services", existingServices).Msg("found services") diff := existingServices.Diff(discoveredServices) for _, removed := range diff.Removed { - deRegisterService(discovery, &removed) + deRegisterService(ctx, discovery, &removed) } for _, added := range diff.Added { - registerService(discovery, &added) + registerService(ctx, discovery, &added) } } -func getExistingServices(clusterName string, store store.Store, includeMaster bool) (register.ServiceInfos, error) { +func getExistingServices( + ctx context.Context, + clusterName string, + store stolonstore.Store, + includeMaster bool, +) (register.ServiceInfos, error) { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) cluster, err := register.NewCluster(clusterName, rCfg, store) if err != nil { return nil, fmt.Errorf("cannot get cluster data: %v", err) @@ -166,7 +225,7 @@ func getExistingServices(clusterName string, store store.Store, includeMaster bo for uid, info := range infos { if !includeMaster && info.IsMaster { - log.Infof("skipping registering master") + logger.Info().Msg("skipping registering master") continue } result[uid] = info @@ -174,24 +233,44 @@ func getExistingServices(clusterName string, store store.Store, includeMaster bo return result, nil } -func registerService(service register.ServiceDiscovery, serviceInfo *register.ServiceInfo) { +func registerService(ctx context.Context, service register.ServiceDiscovery, serviceInfo *register.ServiceInfo) { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) if serviceInfo == nil { return } if err := service.Register(serviceInfo); err != nil { - log.Errorf("unable to register %s with uid %s as %v, reason: %s", serviceInfo.Name, serviceInfo.ID, serviceInfo.Tags, err.Error()) + logger.Error(). + Str("service", serviceInfo.Name). + Str("id", serviceInfo.ID). + Any("tags", serviceInfo.Tags). + AnErr("err", err). + Msg("unable to register %s with uid %s as %v, reason: %s") } else { - log.Infof("successfully registered %s with uid %s as %v", serviceInfo.Name, serviceInfo.ID, serviceInfo.Tags) + logger.Info(). + Str("service", serviceInfo.Name). + Str("id", serviceInfo.ID). + Any("tags", serviceInfo.Tags). + Msg("successfully registered") } } -func deRegisterService(service register.ServiceDiscovery, serviceInfo *register.ServiceInfo) { +func deRegisterService(ctx context.Context, service register.ServiceDiscovery, serviceInfo *register.ServiceInfo) { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) if serviceInfo == nil { return } if err := service.DeRegister(serviceInfo); err != nil { - log.Errorf("unable to deregister %s with uid %s as %v, reason: %s", serviceInfo.Name, serviceInfo.ID, serviceInfo.Tags, err.Error()) + logger.Error(). + Str("service", serviceInfo.Name). + Str("id", serviceInfo.ID). + Any("tags", serviceInfo.Tags). + AnErr("err", err). + Msg("") } else { - log.Infof("successfully deregistered %s with uid %s as %v", serviceInfo.Name, serviceInfo.ID, serviceInfo.Tags) + logger.Info(). + Str("service", serviceInfo.Name). + Str("id", serviceInfo.ID). + Any("tags", serviceInfo.Tags). + Msg("successfully deregistered service") } } diff --git a/cmd/stolonctl/cmd/register/cluster.go b/cmd/stolonctl/cmd/register/cluster.go index 58fc7270d..30afdb943 100644 --- a/cmd/stolonctl/cmd/register/cluster.go +++ b/cmd/stolonctl/cmd/register/cluster.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,27 +13,28 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package register will register all components for stolon-ctl package register import ( "context" "errors" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + stolonstore "github.com/pgvillage-tools/stolon/internal/store" ) // Cluster type exposes necessary methods to find master and slave // from underlying store type Cluster struct { name string - cd *cluster.ClusterData + cd *cluster.Data tagMasterAs Tags tagSlaveAs Tags } // NewCluster returns an new instance of Cluster -func NewCluster(name string, rCfg Config, store store.Store) (*Cluster, error) { +func NewCluster(name string, rCfg Config, store stolonstore.Store) (*Cluster, error) { cd, _, err := store.GetClusterData(context.TODO()) if err != nil { @@ -40,7 +42,8 @@ func NewCluster(name string, rCfg Config, store store.Store) (*Cluster, error) { } else if cd == nil { return nil, errors.New("no cluster data available") } - return &Cluster{name: name, cd: cd, tagMasterAs: NewTags(rCfg.TagMasterAs), tagSlaveAs: NewTags(rCfg.TagSlaveAs)}, nil + return &Cluster{name: name, cd: cd, tagMasterAs: NewTags(rCfg.TagMasterAs), + tagSlaveAs: NewTags(rCfg.TagSlaveAs)}, nil } // ServiceInfos returns all the service information from the cluster data in underlying store diff --git a/cmd/stolonctl/cmd/register/cluster_test.go b/cmd/stolonctl/cmd/register/cluster_test.go index 3fc6a3804..3216d9b12 100644 --- a/cmd/stolonctl/cmd/register/cluster_test.go +++ b/cmd/stolonctl/cmd/register/cluster_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,8 +20,8 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/mock/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + mockstore "github.com/pgvillage-tools/stolon/internal/mock/store" ) func TestNewCluster(t *testing.T) { @@ -28,7 +29,7 @@ func TestNewCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) + mockStore := mockstore.NewMockStore(ctrl) mockStore.EXPECT().GetClusterData(gomock.Any()).Return(nil, nil, errors.New("unable to fetch cluster data")) _, err := NewCluster("test", Config{}, mockStore) @@ -42,7 +43,7 @@ func TestNewCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) + mockStore := mockstore.NewMockStore(ctrl) mockStore.EXPECT().GetClusterData(gomock.Any()).Return(nil, nil, nil) _, err := NewCluster("test", Config{}, mockStore) @@ -55,9 +56,9 @@ func TestNewCluster(t *testing.T) { t.Run("should create new cluster", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - cd := &cluster.ClusterData{} + cd := &cluster.Data{} - mockStore := mock_store.NewMockStore(ctrl) + mockStore := mockstore.NewMockStore(ctrl) mockStore.EXPECT().GetClusterData(gomock.Any()).Return(cd, nil, nil) expected := Cluster{name: "test", cd: cd} @@ -78,7 +79,7 @@ func TestNewCluster(t *testing.T) { func TestServiceInfos(t *testing.T) { t.Run("should return error if cluster data not available", func(t *testing.T) { - cl := Cluster{cd: &cluster.ClusterData{}, name: "test"} + cl := Cluster{cd: &cluster.Data{}, name: "test"} _, err := cl.ServiceInfos() @@ -88,11 +89,14 @@ func TestServiceInfos(t *testing.T) { }) t.Run("should get all the healthy service infos form the cluster data", func(t *testing.T) { - master := &cluster.DB{UID: "master", Status: cluster.DBStatus{Healthy: true, ListenAddress: "127.0.0.1", Port: "5432"}} - slave := &cluster.DB{UID: "slave1", Status: cluster.DBStatus{Healthy: true, ListenAddress: "127.0.0.1", Port: "5433"}} - anotherSlave := &cluster.DB{UID: "slave2", Status: cluster.DBStatus{Healthy: false, ListenAddress: "127.0.0.1", Port: "5433"}} + master := &cluster.DB{UID: "master", Status: cluster.DBStatus{Healthy: true, + ListenAddress: "127.0.0.1", Port: "5432"}} + slave := &cluster.DB{UID: "slave1", Status: cluster.DBStatus{Healthy: true, + ListenAddress: "127.0.0.1", Port: "5433"}} + anotherSlave := &cluster.DB{UID: "slave2", Status: cluster.DBStatus{Healthy: false, + ListenAddress: "127.0.0.1", Port: "5433"}} cl := Cluster{ - cd: &cluster.ClusterData{ + cd: &cluster.Data{ DBs: map[string]*cluster.DB{"master": master, "slave1": slave, "slave2": anotherSlave}, Cluster: &cluster.Cluster{ Status: cluster.ClusterStatus{Master: "master"}, diff --git a/cmd/stolonctl/cmd/register/config.go b/cmd/stolonctl/cmd/register/config.go index 201d1f86f..723c96d85 100644 --- a/cmd/stolonctl/cmd/register/config.go +++ b/cmd/stolonctl/cmd/register/config.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,13 +59,13 @@ func (config *Config) Validate() error { // ConsulConfig returns consul.api.ConsulConfig if register endpoint is valid consul url // else will return error with appropriate reason func (config *Config) ConsulConfig() (*api.Config, error) { - url, err := url.Parse(config.Endpoints) + parsed, err := url.Parse(config.Endpoints) if err != nil { return nil, err } return &api.Config{ - Address: url.Host, - Scheme: url.Scheme, + Address: parsed.Host, + Scheme: parsed.Scheme, TLSConfig: api.TLSConfig{ Address: config.TLSAddress, CAFile: config.TLSCAFile, diff --git a/cmd/stolonctl/cmd/register/config_test.go b/cmd/stolonctl/cmd/register/config_test.go index 3d70329ed..81c105420 100644 --- a/cmd/stolonctl/cmd/register/config_test.go +++ b/cmd/stolonctl/cmd/register/config_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +40,8 @@ func TestRegisterConfig(t *testing.T) { config := Config{Backend: "consul", Endpoints: "http://127.0.0.1:8500,http://127.0.0.2:8500"} err := config.Validate() - if err == nil || err.Error() != "consul does not support multiple endpoints: http://127.0.0.1:8500,http://127.0.0.2:8500" { + if err == nil || err.Error() != + "consul does not support multiple endpoints: http://127.0.0.1:8500,http://127.0.0.2:8500" { t.Errorf("expected unknown register backend but got %s", err.Error()) } }) diff --git a/cmd/stolonctl/cmd/register/discovery.go b/cmd/stolonctl/cmd/register/discovery.go index 1b40b85aa..e6040bfc7 100644 --- a/cmd/stolonctl/cmd/register/discovery.go +++ b/cmd/stolonctl/cmd/register/discovery.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,15 +32,17 @@ type ServiceDiscovery interface { func NewServiceDiscovery(config *Config) (ServiceDiscovery, error) { switch config.Backend { case "consul": - if apiConfig, err := config.ConsulConfig(); err != nil { + apiConfig, err := config.ConsulConfig() + if err != nil { return nil, err - } else if client, err := api.NewClient(apiConfig); err != nil { + } + client, err := api.NewClient(apiConfig) + if err != nil { return nil, err - } else { - agent := client.Agent() - catalog := client.Catalog() - return NewConsulServiceDiscovery(agent, catalog), nil } + agent := client.Agent() + catalog := client.Catalog() + return NewConsulServiceDiscovery(agent, catalog), nil default: return nil, errors.New("register backend not supported") } @@ -57,6 +60,7 @@ type ConsulAgent interface { ServiceDeregister(serviceID string) error } +// ConsulCatalog defines a catalog as registered in consul type ConsulCatalog interface { Service(service, tag string, q *api.QueryOptions) ([]*api.CatalogService, *api.QueryMeta, error) } diff --git a/cmd/stolonctl/cmd/register/discovery_test.go b/cmd/stolonctl/cmd/register/discovery_test.go index 717ad1f7d..e20d6b356 100644 --- a/cmd/stolonctl/cmd/register/discovery_test.go +++ b/cmd/stolonctl/cmd/register/discovery_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,8 +21,8 @@ import ( "github.com/golang/mock/gomock" "github.com/hashicorp/consul/api" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/internal/mock/register" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" + mock_register "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/internal/mock/register" + "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/register" ) func TestNewServiceDiscovery(t *testing.T) { diff --git a/cmd/stolonctl/cmd/register/serviceinfo.go b/cmd/stolonctl/cmd/register/serviceinfo.go index dca20be65..72fc1e743 100644 --- a/cmd/stolonctl/cmd/register/serviceinfo.go +++ b/cmd/stolonctl/cmd/register/serviceinfo.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +23,7 @@ import ( "strings" "github.com/hashicorp/consul/api" - "github.com/sorintlab/stolon/internal/cluster" + cluster "github.com/pgvillage-tools/stolon/api/v1" ) // HealthCheck holds necessary information for performing @@ -87,7 +88,7 @@ func (info *ServiceInfo) Compare(target ServiceInfo) bool { func NewServiceInfo(name string, db *cluster.DB, tags []string, isMaster bool) (*ServiceInfo, error) { port, err := strconv.Atoi(db.Status.Port) if err != nil { - return nil, fmt.Errorf(fmt.Sprintf("invalid database port '%s' for %s with uid %s", db.Status.Port, name, db.UID)) + return nil, fmt.Errorf("invalid database port '%s' for %s with uid %s", db.Status.Port, name, db.UID) } return &ServiceInfo{ Name: name, diff --git a/cmd/stolonctl/cmd/register/serviceinfo_test.go b/cmd/stolonctl/cmd/register/serviceinfo_test.go index 6422ce898..5044f7fbf 100644 --- a/cmd/stolonctl/cmd/register/serviceinfo_test.go +++ b/cmd/stolonctl/cmd/register/serviceinfo_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +20,7 @@ import ( "testing" "github.com/hashicorp/consul/api" - "github.com/sorintlab/stolon/internal/cluster" + cluster "github.com/pgvillage-tools/stolon/api/v1" ) func TestNewServiceInfo(t *testing.T) { @@ -79,8 +80,7 @@ func TestConsulAgentServiceRegistration(t *testing.T) { if actual == nil { t.Errorf("expected consul agent service registration not to be nil") - } - if actual.ID != service.ID { + } else if actual.ID != service.ID { t.Errorf("expected id to be %s but was %s", service.ID, actual.ID) } else if actual.Name != service.Name { t.Errorf("expected name to be %s but was %s", service.Name, actual.Name) @@ -277,7 +277,6 @@ func TestServiceInfosDiff(t *testing.T) { if len(diff.Removed) != 0 { t.Errorf("expected no service to be removed but %d service got removed", len(diff.Removed)) } - }) t.Run("should only add when new services are found", func(t *testing.T) { @@ -299,11 +298,13 @@ func TestServiceInfosDiff(t *testing.T) { t.Errorf("expected %s to be added but not", id) } } - }) t.Run("should only remove when discovered services no longer exists", func(t *testing.T) { - discoveredServiceInfos := ServiceInfos{"masterUID": ServiceInfo{ID: "masterUID"}, "slaveUID": ServiceInfo{ID: "slaveUID"}} + discoveredServiceInfos := ServiceInfos{ + "masterUID": ServiceInfo{ID: "masterUID"}, + "slaveUID": ServiceInfo{ID: "slaveUID"}, + } existingServiceInfos := ServiceInfos{} diff := existingServiceInfos.Diff(discoveredServiceInfos) @@ -321,7 +322,6 @@ func TestServiceInfosDiff(t *testing.T) { t.Errorf("expected %s to be removed but not", id) } } - }) t.Run("should not add or remove when discovered and existing services are same", func(t *testing.T) { @@ -336,12 +336,17 @@ func TestServiceInfosDiff(t *testing.T) { if len(diff.Removed) != 0 { t.Errorf("expected no service to be removed but %d service got removed", len(diff.Removed)) } - }) t.Run("should add and remove corresponding service infos", func(t *testing.T) { - discoveredServiceInfos := ServiceInfos{"masterUID": ServiceInfo{ID: "masterUID"}, "slaveUID": ServiceInfo{ID: "slaveUID"}} - existingServiceInfos := ServiceInfos{"newSlaveUID": ServiceInfo{ID: "newSlaveUID"}, "slaveUID": ServiceInfo{ID: "slaveUID"}} + discoveredServiceInfos := ServiceInfos{ + "masterUID": ServiceInfo{ID: "masterUID"}, + "slaveUID": ServiceInfo{ID: "slaveUID"}, + } + existingServiceInfos := ServiceInfos{ + "newSlaveUID": ServiceInfo{ID: "newSlaveUID"}, + "slaveUID": ServiceInfo{ID: "slaveUID"}, + } diff := existingServiceInfos.Diff(discoveredServiceInfos) @@ -368,10 +373,16 @@ func TestServiceInfosDiff(t *testing.T) { }) t.Run("should add and remove corresponding service infos", func(t *testing.T) { - discoveredServiceInfos := ServiceInfos{"masterUID": ServiceInfo{ID: "masterUID", Tags: Tags{"master"}}, "slaveUID": ServiceInfo{ID: "slaveUID"}, - "anotherSlaveUID": ServiceInfo{ID: "anotherSlaveUID"}} - existingServiceInfos := ServiceInfos{"masterUID": ServiceInfo{ID: "masterUID", Tags: Tags{"slave"}}, "slaveUID": ServiceInfo{ID: "slaveUID"}, - "anotherSlaveUID": ServiceInfo{ID: "anotherSlaveUID", Tags: Tags{"master"}}} + discoveredServiceInfos := ServiceInfos{ + "masterUID": ServiceInfo{ID: "masterUID", Tags: Tags{"master"}}, + "slaveUID": ServiceInfo{ID: "slaveUID"}, + "anotherSlaveUID": ServiceInfo{ID: "anotherSlaveUID"}, + } + existingServiceInfos := ServiceInfos{ + "masterUID": ServiceInfo{ID: "masterUID", Tags: Tags{"slave"}}, + "slaveUID": ServiceInfo{ID: "slaveUID"}, + "anotherSlaveUID": ServiceInfo{ID: "anotherSlaveUID", Tags: Tags{"master"}}, + } diff := existingServiceInfos.Diff(discoveredServiceInfos) @@ -395,7 +406,6 @@ func TestServiceInfosDiff(t *testing.T) { t.Errorf("expected %s to be added but not", id) } } - }) } diff --git a/cmd/stolonctl/cmd/register_test.go b/cmd/stolonctl/cmd/register_test.go index 85a6aa0f7..c9b4d25c4 100644 --- a/cmd/stolonctl/cmd/register_test.go +++ b/cmd/stolonctl/cmd/register_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2019 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,19 +15,20 @@ package cmd import ( - "github.com/golang/mock/gomock" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/internal/mock/register" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/mock/store" - "github.com/sorintlab/stolon/internal/store" + "context" "testing" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" + "github.com/golang/mock/gomock" + cluster "github.com/pgvillage-tools/stolon/api/v1" + mockregister "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/internal/mock/register" + mockstore "github.com/pgvillage-tools/stolon/internal/mock/store" + "github.com/pgvillage-tools/stolon/internal/store" + + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd/register" ) func TestCheckConfig(t *testing.T) { - t.Run("should check for cluster name", func(t *testing.T) { c := config{} rc := register.Config{} @@ -69,12 +71,13 @@ func TestCheckConfig(t *testing.T) { } func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { + ctx := context.Background() t.Run("should deregister all the discovered services", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Tags: []string{"slave"}} @@ -85,7 +88,7 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { } mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{}, DBs: cluster.DBs{}, } @@ -93,23 +96,35 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockServiceDiscovery.EXPECT().DeRegister(&anotherServiceInfo) mockServiceDiscovery.EXPECT().DeRegister(&serviceInfo) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) }) t.Run("should register all the existing services", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" - serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} - anotherServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} + serviceInfo := register.ServiceInfo{ + Name: clusterName, + ID: "uid1", + Port: 5432, + Tags: []string{"slave"}, + Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}, + } + anotherServiceInfo := register.ServiceInfo{ + Name: clusterName, + Port: 5433, + ID: "uid2", + Tags: []string{"slave"}, + Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}, + } discoveredServices := register.ServiceInfos{} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{}, DBs: cluster.DBs{ "uid1": &cluster.DB{ @@ -126,57 +141,64 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockServiceDiscovery.EXPECT().Register(&serviceInfo) mockServiceDiscovery.EXPECT().Register(&anotherServiceInfo) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) }) - t.Run("should register existing services and deregister the discovered service which are no longer available", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + t.Run("should register existing services and deregister the discovered service which are no longer available", + func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) - clusterName := "test-cluster" - serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} - anotherServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} - yetAnotherServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5434, ID: "uid3", Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} - discoveredServices := register.ServiceInfos{"uid1": serviceInfo, "uid2": anotherServiceInfo, "uid3": yetAnotherServiceInfo} + clusterName := "test-cluster" + serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} + anotherServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} + yetAnotherServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5434, ID: "uid3", + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} + discoveredServices := register.ServiceInfos{"uid1": serviceInfo, "uid2": anotherServiceInfo, + "uid3": yetAnotherServiceInfo} - mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ - Cluster: &cluster.Cluster{}, - DBs: cluster.DBs{ - "uid1": &cluster.DB{ - UID: "uid1", - Status: cluster.DBStatus{Port: "5432", Healthy: true}, - }, - "uid2": &cluster.DB{ - UID: "uid2", - Status: cluster.DBStatus{Port: "5433", Healthy: true}, + mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) + clusterData := cluster.Data{ + Cluster: &cluster.Cluster{}, + DBs: cluster.DBs{ + "uid1": &cluster.DB{ + UID: "uid1", + Status: cluster.DBStatus{Port: "5432", Healthy: true}, + }, + "uid2": &cluster.DB{ + UID: "uid2", + Status: cluster.DBStatus{Port: "5433", Healthy: true}, + }, }, - }, - } - mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) - mockServiceDiscovery.EXPECT().DeRegister(&yetAnotherServiceInfo) + } + mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) + mockServiceDiscovery.EXPECT().DeRegister(&yetAnotherServiceInfo) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) - }) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) + }) t.Run("master registration is not allowed", func(t *testing.T) { t.Run("should deregister the master even it exists", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" - serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} - masterServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}, IsMaster: true} + serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} + masterServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}, IsMaster: true} discoveredServices := register.ServiceInfos{"uid1": serviceInfo, "uid2": masterServiceInfo} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{Status: cluster.ClusterStatus{Master: "uid2"}}, DBs: cluster.DBs{ "uid1": &cluster.DB{ @@ -192,22 +214,23 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) mockServiceDiscovery.EXPECT().DeRegister(&masterServiceInfo) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) }) t.Run("should deregister if discovered service is master", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" - masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5434, ID: "uid2", Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} + masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5434, ID: "uid2", + Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} discoveredServices := register.ServiceInfos{"uid3": masterService} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{Status: cluster.ClusterStatus{Master: "uid2"}}, DBs: cluster.DBs{ "uid2": &cluster.DB{ @@ -219,21 +242,21 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) mockServiceDiscovery.EXPECT().DeRegister(&masterService) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) }) t.Run("should not register if existing service is master", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" discoveredServices := register.ServiceInfos{} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{Status: cluster.ClusterStatus{Master: "uid1"}}, DBs: cluster.DBs{ "uid1": &cluster.DB{ @@ -244,7 +267,7 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { } mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, false) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, false) }) }) @@ -253,16 +276,18 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" - serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} - masterServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}, IsMaster: true} + serviceInfo := register.ServiceInfo{Name: clusterName, ID: "uid1", Port: 5432, + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} + masterServiceInfo := register.ServiceInfo{Name: clusterName, Port: 5433, ID: "uid2", + Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}, IsMaster: true} discoveredServices := register.ServiceInfos{"uid1": serviceInfo} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{Status: cluster.ClusterStatus{Master: "uid2"}}, DBs: cluster.DBs{ "uid1": &cluster.DB{ @@ -278,44 +303,46 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) mockServiceDiscovery.EXPECT().Register(&masterServiceInfo) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, true) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, true) }) t.Run("should deregister if non existing is master", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" - masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5434, ID: "uid3", Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} + masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5434, ID: "uid3", + Tags: []string{"slave"}, Check: register.HealthCheck{TCP: ":5433", Interval: "10s"}} discoveredServices := register.ServiceInfos{"uid3": masterService} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{}, DBs: cluster.DBs{}, } mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) mockServiceDiscovery.EXPECT().DeRegister(&masterService) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, true) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, true) }) t.Run("should register if existing service is master", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockStore := mock_store.NewMockStore(ctrl) - mockServiceDiscovery := mock_register.NewMockServiceDiscovery(ctrl) + mockStore := mockstore.NewMockStore(ctrl) + mockServiceDiscovery := mockregister.NewMockServiceDiscovery(ctrl) clusterName := "test-cluster" discoveredServices := register.ServiceInfos{} - masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5432, ID: "uid1", Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} + masterService := register.ServiceInfo{IsMaster: true, Name: clusterName, Port: 5432, ID: "uid1", + Tags: []string{"master"}, Check: register.HealthCheck{TCP: ":5432", Interval: "10s"}} mockServiceDiscovery.EXPECT().Services(clusterName).Return(discoveredServices, nil) - clusterData := cluster.ClusterData{ + clusterData := cluster.Data{ Cluster: &cluster.Cluster{Status: cluster.ClusterStatus{Master: "uid1"}}, DBs: cluster.DBs{ "uid1": &cluster.DB{ @@ -327,7 +354,7 @@ func TestCheckAndRegisterMasterAndSlaves(t *testing.T) { mockStore.EXPECT().GetClusterData(gomock.Any()).Return(&clusterData, &store.KVPair{}, nil) mockServiceDiscovery.EXPECT().Register(&masterService) - checkAndRegisterMasterAndSlaves(clusterName, mockStore, mockServiceDiscovery, true) + checkAndRegisterMasterAndSlaves(ctx, clusterName, mockStore, mockServiceDiscovery, true) }) }) } diff --git a/cmd/stolonctl/cmd/removekeeper.go b/cmd/stolonctl/cmd/removekeeper.go index 93c5f3142..5807f312d 100644 --- a/cmd/stolonctl/cmd/removekeeper.go +++ b/cmd/stolonctl/cmd/removekeeper.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +18,7 @@ package cmd import ( "context" - cmdcommon "github.com/sorintlab/stolon/cmd" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" "github.com/spf13/cobra" ) @@ -32,6 +33,8 @@ func init() { } func removeKeeper(_ *cobra.Command, args []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() if len(args) > 1 { die("too many arguments") } @@ -42,7 +45,7 @@ func removeKeeper(_ *cobra.Command, args []string) { keeperID := args[0] - store, err := cmdcommon.NewStore(&cfg.CommonConfig) + store, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } diff --git a/cmd/stolonctl/cmd/spec.go b/cmd/stolonctl/cmd/spec.go index 5649c2710..84f794134 100644 --- a/cmd/stolonctl/cmd/spec.go +++ b/cmd/stolonctl/cmd/spec.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,10 +16,11 @@ package cmd import ( + "context" "encoding/json" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" "github.com/spf13/cobra" ) @@ -41,6 +43,7 @@ func init() { CmdStolonCtl.AddCommand(cmdSpec) } +// ClusterSpecNoDefaults is used to mashal a json with a cluster spec if no defaults should be used type ClusterSpecNoDefaults struct { SleepInterval *cluster.Duration `json:"sleepInterval,omitempty"` RequestTimeout *cluster.Duration `json:"requestTimeout,omitempty"` @@ -61,9 +64,9 @@ type ClusterSpecNoDefaults struct { AdditionalWalSenders *uint16 `json:"additionalWalSenders,omitempty"` AdditionalMasterReplicationSlots []string `json:"additionalMasterReplicationSlots,omitempty"` UsePgrewind *bool `json:"usePgrewind,omitempty"` - InitMode *cluster.ClusterInitMode `json:"initMode,omitempty"` + InitMode *cluster.InitMode `json:"initMode,omitempty"` MergePgParameters *bool `json:"mergePgParameters,omitempty"` - Role *cluster.ClusterRole `json:"role,omitempty"` + Role *cluster.Role `json:"role,omitempty"` NewConfig *cluster.NewConfig `json:"newConfig,omitempty"` PITRConfig *cluster.PITRConfig `json:"pitrConfig,omitempty"` ExistingConfig *cluster.ExistingConfig `json:"existingConfig,omitempty"` @@ -74,6 +77,7 @@ type ClusterSpecNoDefaults struct { AutomaticPgRestart *bool `json:"automaticPgRestart,omitempty"` } +// ClusterSpecDefaults is used to mashal a json with a cluster spec if defaults should be used type ClusterSpecDefaults struct { SleepInterval *cluster.Duration `json:"sleepInterval"` RequestTimeout *cluster.Duration `json:"requestTimeout"` @@ -94,9 +98,9 @@ type ClusterSpecDefaults struct { AdditionalWalSenders *uint16 `json:"additionalWalSenders"` AdditionalMasterReplicationSlots []string `json:"additionalMasterReplicationSlots"` UsePgrewind *bool `json:"usePgrewind"` - InitMode *cluster.ClusterInitMode `json:"initMode"` + InitMode *cluster.InitMode `json:"initMode"` MergePgParameters *bool `json:"mergePgParameters"` - Role *cluster.ClusterRole `json:"role"` + Role *cluster.Role `json:"role"` NewConfig *cluster.NewConfig `json:"newConfig"` PITRConfig *cluster.PITRConfig `json:"pitrConfig"` ExistingConfig *cluster.ExistingConfig `json:"existingConfig"` @@ -107,8 +111,10 @@ type ClusterSpecDefaults struct { AutomaticPgRestart *bool `json:"automaticPgRestart"` } -func spec(cmd *cobra.Command, args []string) { - e, err := cmdcommon.NewStore(&cfg.CommonConfig) +func spec(_ *cobra.Command, _ []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } diff --git a/cmd/stolonctl/cmd/status.go b/cmd/stolonctl/cmd/status.go index 599f4a9cb..cfe9fab78 100644 --- a/cmd/stolonctl/cmd/status.go +++ b/cmd/stolonctl/cmd/status.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,30 +23,34 @@ import ( "sort" "text/tabwriter" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/logging" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" ) +const ( + tabWidth = 8 +) + var cmdStatus = &cobra.Command{ Use: "status", Run: status, Short: "Display the current cluster status", } -type StatusOptions struct { +var statusOpts struct { Format string } -var statusOpts StatusOptions - func init() { cmdStatus.PersistentFlags().StringVarP(&statusOpts.Format, "format", "f", "", "output format") CmdStolonCtl.AddCommand(cmdStatus) } +// Status stores that state of all sentinels, proxies, keepers and the cluster type Status struct { Sentinels []SentinelStatus `json:"sentinels"` Proxies []ProxyStatus `json:"proxies"` @@ -53,16 +58,19 @@ type Status struct { Cluster ClusterStatus `json:"cluster"` } +// SentinelStatus stores the status of the Sentinel type SentinelStatus struct { UID string `json:"uid"` Leader bool `json:"leader"` } +// ProxyStatus stores the status of the Proxy type ProxyStatus struct { UID string `json:"uid"` Generation int64 `json:"generation"` } +// KeeperStatus stores the status of the Keeper type KeeperStatus struct { UID string `json:"uid"` ListenAddress string `json:"listen_address"` @@ -72,27 +80,30 @@ type KeeperStatus struct { PgCurrentGeneration int64 `json:"pg_current_generation"` } +// ClusterStatus stores the status of the CLuster type ClusterStatus struct { Available bool `json:"available"` MasterKeeperUID string `json:"master_keeper_uid"` MasterDBUID string `json:"master_db_uid"` } -func status(cmd *cobra.Command, args []string) { - status, generateErr := generateStatus() +func status(_ *cobra.Command, _ []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + status, generateErr := generateStatus(ctx) switch statusOpts.Format { case "json": - renderJSON(status, generateErr) + renderJSON(ctx, status, generateErr) case "text": - renderText(status, generateErr) + renderText(ctx, status, generateErr) case "": - renderText(status, generateErr) + renderText(ctx, status, generateErr) default: die("unrecognised output format %s", statusOpts.Format) } } -func renderJSON(status Status, generateErr error) { +func renderJSON(_ context.Context, status Status, generateErr error) { if generateErr != nil { marshalJSON(generateErr) } else { @@ -100,7 +111,7 @@ func renderJSON(status Status, generateErr error) { } } -func marshalJSON(value interface{}) { +func marshalJSON(value any) { output, err := json.MarshalIndent(value, "", "\t") if err != nil { die("failed to marshal error: %v", err) @@ -108,23 +119,36 @@ func marshalJSON(value interface{}) { stdout("%s", output) } -func renderText(status Status, generateErr error) { +func tabPrint(ctx context.Context, tw *tabwriter.Writer, formatted string, args ...any) { + if _, err := fmt.Fprintf(tw, formatted, args...); err != nil { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) + logger.Fatal().AnErr("err", err).Msg("failed to write to tab writer") + } +} + +func tabFlush(ctx context.Context, tw *tabwriter.Writer) { + if err := tw.Flush(); err != nil { + _, logger := logging.GetLogComponent(ctx, logging.CmdComponent) + logger.Fatal().AnErr("err", err).Msg("failed to flush tab writer") + } +} +func renderText(ctx context.Context, status Status, generateErr error) { if generateErr != nil { die("%v", generateErr) } tabOut := new(tabwriter.Writer) - tabOut.Init(os.Stdout, 0, 8, 1, '\t', 0) + tabOut.Init(os.Stdout, 0, tabWidth, 1, '\t', 0) stdout("=== Active sentinels ===") stdout("") if len(status.Sentinels) == 0 { stdout("No active sentinels") } else { - fmt.Fprintf(tabOut, "ID\tLEADER\n") + tabPrint(ctx, tabOut, "ID\tLEADER\n") for _, s := range status.Sentinels { - fmt.Fprintf(tabOut, "%s\t%t\n", s.UID, s.Leader) - tabOut.Flush() + tabPrint(ctx, tabOut, "%s\t%t\n", s.UID, s.Leader) + tabFlush(ctx, tabOut) } } @@ -134,10 +158,10 @@ func renderText(status Status, generateErr error) { if len(status.Proxies) == 0 { stdout("No active proxies") } else { - fmt.Fprintf(tabOut, "ID\n") + tabPrint(ctx, tabOut, "ID\n") for _, p := range status.Proxies { - fmt.Fprintf(tabOut, "%s\n", p.UID) - tabOut.Flush() + tabPrint(ctx, tabOut, "%s\n", p.UID) + tabFlush(ctx, tabOut) } } @@ -148,10 +172,21 @@ func renderText(status Status, generateErr error) { stdout("No keepers available") stdout("") } else { - fmt.Fprintf(tabOut, "UID\tHEALTHY\tPG LISTENADDRESS\tPG HEALTHY\tPG WANTEDGENERATION\tPG CURRENTGENERATION\n") + tabPrint(ctx, tabOut, + "UID\tHEALTHY\tPG LISTENADDRESS\tPG HEALTHY\tPG WANTEDGENERATION\tPG CURRENTGENERATION\n") for _, k := range status.Keepers { - fmt.Fprintf(tabOut, "%s\t%t\t%s\t%t\t%d\t%d\t\n", k.UID, k.Healthy, k.ListenAddress, k.PgHealthy, k.PgWantedGeneration, k.PgCurrentGeneration) - tabOut.Flush() + tabPrint( + ctx, + tabOut, + "%s\t%t\t%s\t%t\t%d\t%d\t\n", + k.UID, + k.Healthy, + k.ListenAddress, + k.PgHealthy, + k.PgWantedGeneration, + k.PgCurrentGeneration, + ) + tabFlush(ctx, tabOut) } } @@ -169,7 +204,7 @@ func renderText(status Status, generateErr error) { } // This tree data isn't currently available in the Status struct - e, err := cmdcommon.NewStore(&cfg.CommonConfig) + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } @@ -186,7 +221,7 @@ func renderText(status Status, generateErr error) { stdout("") } -func printTree(dbuid string, cd *cluster.ClusterData, level int, prefix string, tail bool) { +func printTree(dbuid string, cd *cluster.Data, level int, prefix string, tail bool) { // skip not existing db: specified as a follower but not available in the // cluster spec (this should happen only when doing a stolonctl // removekeeper) @@ -205,7 +240,7 @@ func printTree(dbuid string, cd *cluster.ClusterData, level int, prefix string, if dbuid == cd.Cluster.Status.Master { out += " (master)" } - stdout(out) + stdout("%s", out) db := cd.DBs[dbuid] followers := db.Spec.Followers c := len(followers) @@ -231,17 +266,17 @@ func printTree(dbuid string, cd *cluster.ClusterData, level int, prefix string, } } -func generateStatus() (Status, error) { +func generateStatus(ctx context.Context) (Status, error) { status := Status{} tabOut := new(tabwriter.Writer) - tabOut.Init(os.Stdout, 0, 8, 1, '\t', 0) + tabOut.Init(os.Stdout, 0, tabWidth, 1, '\t', 0) - e, err := cmdcommon.NewStore(&cfg.CommonConfig) + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { return status, err } - election, err := cmdcommon.NewElection(&cfg.CommonConfig, "") + election, err := cmdcommon.NewElection(ctx, &cfg.CommonConfig, "") if err != nil { return status, err } @@ -256,7 +291,7 @@ func generateStatus() (Status, error) { return status, err } - sentinels := make([]SentinelStatus, 0) + sentinels := []SentinelStatus{} sort.Sort(sentinelsInfo) for _, si := range sentinelsInfo { leader := lsid != "" && si.UID == lsid @@ -270,7 +305,7 @@ func generateStatus() (Status, error) { } proxiesInfoSlice := proxiesInfo.ToSlice() - proxies := make([]ProxyStatus, 0) + proxies := []ProxyStatus{} sort.Sort(proxiesInfoSlice) for _, pi := range proxiesInfoSlice { proxies = append(proxies, ProxyStatus{UID: pi.UID, Generation: pi.Generation}) @@ -282,7 +317,7 @@ func generateStatus() (Status, error) { return status, err } - keepers := make([]KeeperStatus, 0) + keepers := []KeeperStatus{} kssKeys := cd.Keepers.SortedKeys() for _, kuid := range kssKeys { k := cd.Keepers[kuid] @@ -315,19 +350,19 @@ func generateStatus() (Status, error) { } status.Keepers = keepers - cluster := ClusterStatus{} + clusterStatus := ClusterStatus{} if cd.Cluster == nil || cd.DBs == nil { - cluster.Available = false + clusterStatus.Available = false } else { master := cd.Cluster.Status.Master - cluster.Available = true + clusterStatus.Available = true if master != "" { - cluster.MasterDBUID = cd.DBs[master].UID - cluster.MasterKeeperUID = cd.Keepers[cd.DBs[master].Spec.KeeperUID].UID + clusterStatus.MasterDBUID = cd.DBs[master].UID + clusterStatus.MasterKeeperUID = cd.Keepers[cd.DBs[master].Spec.KeeperUID].UID } } - status.Cluster = cluster + status.Cluster = clusterStatus return status, nil } diff --git a/cmd/stolonctl/cmd/stolonctl.go b/cmd/stolonctl/cmd/stolonctl.go index 6cc379100..ce57ebe23 100644 --- a/cmd/stolonctl/cmd/stolonctl.go +++ b/cmd/stolonctl/cmd/stolonctl.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,13 +19,15 @@ import ( "bufio" "context" "fmt" + "log" "os" "strings" - "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/flagutil" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/flagutil" + "github.com/pgvillage-tools/stolon/internal/logging" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" ) @@ -33,19 +36,20 @@ const ( maxRetries = 3 ) +// CmdStolonCtl defines a cobra command to execute when running stolonctl var CmdStolonCtl = &cobra.Command{ Use: "stolonctl", Short: "stolon command line client", Version: cmd.Version, - PersistentPreRun: func(c *cobra.Command, args []string) { + PersistentPreRun: func(c *cobra.Command, _ []string) { if c.Name() != "stolonctl" && c.Name() != "version" { if err := cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { - die(err.Error()) + die("%s", err.Error()) } } }, // just defined to make --version work - Run: func(c *cobra.Command, args []string) { _ = c.Help() }, + Run: func(c *cobra.Command, _ []string) { _ = c.Help() }, } type config struct { @@ -60,8 +64,10 @@ func init() { } var cmdVersion = &cobra.Command{ - Use: "version", - Run: versionCommand, + Use: "version", + Run: func(_ *cobra.Command, _ []string) { + stdout("stolonctl version %s", cmd.Version) + }, Short: "Display the version", } @@ -69,35 +75,35 @@ func init() { CmdStolonCtl.AddCommand(cmdVersion) } -func versionCommand(c *cobra.Command, args []string) { - stdout("stolonctl version %s", cmd.Version) -} - +// Execute is run when stolonctl is executed func Execute() { + _, logger := logging.GetLogComponent(context.Background(), logging.CmdComponent) if err := flagutil.SetFlagsFromEnv(CmdStolonCtl.PersistentFlags(), "STOLONCTL"); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } if err := CmdStolonCtl.Execute(); err != nil { - log.Fatal(err) + logger.Fatal().AnErr("err", err).Msg("") } } -func stderr(format string, a ...interface{}) { +func stderr(format string, a ...any) { out := fmt.Sprintf(format, a...) fmt.Fprintln(os.Stderr, strings.TrimSuffix(out, "\n")) } -func stdout(format string, a ...interface{}) { +func stdout(format string, a ...any) { out := fmt.Sprintf(format, a...) - fmt.Fprintln(os.Stdout, strings.TrimSuffix(out, "\n")) + if _, err := fmt.Fprintln(os.Stdout, strings.TrimSuffix(out, "\n")); err != nil { + log.Fatalf("failed to write to stdout: %v", err) + } } -func die(format string, a ...interface{}) { +func die(format string, a ...any) { stderr(format, a...) os.Exit(1) } -func getClusterData(e store.Store) (*cluster.ClusterData, *store.KVPair, error) { +func getClusterData(e store.Store) (*cluster.Data, *store.KVPair, error) { cd, pair, err := e.GetClusterData(context.TODO()) if err != nil { return nil, nil, fmt.Errorf("cannot get cluster data: %v", err) @@ -117,7 +123,9 @@ func getClusterData(e store.Store) (*cluster.ClusterData, *store.KVPair, error) func askConfirmation(message string) (bool, error) { in := bufio.NewReader(os.Stdin) for { - fmt.Fprint(os.Stdout, message) + if _, err := fmt.Fprint(os.Stdout, message); err != nil { + log.Fatalf("failed to print to stdout: %v", err) + } input, err := in.ReadString('\n') if err != nil { return false, fmt.Errorf("error reading input: %v", err) diff --git a/cmd/stolonctl/cmd/update.go b/cmd/stolonctl/cmd/update.go index e7097b413..87a8a6cbc 100644 --- a/cmd/stolonctl/cmd/update.go +++ b/cmd/stolonctl/cmd/update.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,12 +19,12 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "os" - cmdcommon "github.com/sorintlab/stolon/cmd" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + cmdcommon "github.com/pgvillage-tools/stolon/cmd" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/util/strategicpatch" @@ -43,30 +44,34 @@ type updateOptions struct { var updateOpts updateOptions func init() { - cmdUpdate.PersistentFlags().BoolVarP(&updateOpts.patch, "patch", "p", false, "patch the current cluster specification instead of replacing it") - cmdUpdate.PersistentFlags().StringVarP(&updateOpts.file, "file", "f", "", "file containing a complete cluster specification or a patch to apply to the current cluster specification") + cmdUpdate.PersistentFlags().BoolVarP(&updateOpts.patch, "patch", "p", false, + "patch the current cluster specification instead of replacing it") + cmdUpdate.PersistentFlags().StringVarP(&updateOpts.file, "file", "f", "", + "file containing a complete cluster specification or a patch to apply to the current cluster specification") CmdStolonCtl.AddCommand(cmdUpdate) } -func patchClusterSpec(cs *cluster.ClusterSpec, p []byte) (*cluster.ClusterSpec, error) { +func patchClusterSpec(cs *cluster.Spec, p []byte) (*cluster.Spec, error) { csj, err := json.Marshal(cs) if err != nil { return nil, fmt.Errorf("failed to marshal cluster spec: %v", err) } - newcsj, err := strategicpatch.StrategicMergePatch(csj, p, &cluster.ClusterSpec{}) + newcsj, err := strategicpatch.StrategicMergePatch(csj, p, &cluster.Spec{}) if err != nil { return nil, fmt.Errorf("failed to merge patch cluster spec: %v", err) } - var newcs *cluster.ClusterSpec + var newcs *cluster.Spec if err := json.Unmarshal(newcsj, &newcs); err != nil { return nil, fmt.Errorf("failed to unmarshal patched cluster spec: %v", err) } return newcs, nil } -func update(cmd *cobra.Command, args []string) { +func update(_ *cobra.Command, args []string) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() if len(args) > 1 { die("too many arguments") } @@ -83,19 +88,19 @@ func update(cmd *cobra.Command, args []string) { } else { var err error if updateOpts.file == "-" { - data, err = ioutil.ReadAll(os.Stdin) + data, err = io.ReadAll(os.Stdin) if err != nil { die("cannot read from stdin: %v", err) } } else { - data, err = ioutil.ReadFile(updateOpts.file) + data, err = os.ReadFile(updateOpts.file) if err != nil { die("cannot read file: %v", err) } } } - e, err := cmdcommon.NewStore(&cfg.CommonConfig) + e, err := cmdcommon.NewStore(ctx, &cfg.CommonConfig) if err != nil { die("%v", err) } @@ -113,7 +118,7 @@ func update(cmd *cobra.Command, args []string) { die("no cluster spec available") } - var newcs *cluster.ClusterSpec + var newcs *cluster.Spec if updateOpts.patch { newcs, err = patchClusterSpec(cd.Cluster.Spec, data) if err != nil { diff --git a/cmd/stolonctl/main.go b/cmd/stolonctl/main.go index 29d9c14ee..8feef88fb 100644 --- a/cmd/stolonctl/main.go +++ b/cmd/stolonctl/main.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package main is a package that provides functionality concerning postgresSQL lifecycles package main import ( - "github.com/sorintlab/stolon/cmd/stolonctl/cmd" + "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd" ) func main() { diff --git a/cmd/version.go b/cmd/version.go index 7b04eab72..dba8c0540 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,4 +15,5 @@ package cmd +// Version is a placeholder for the actual version, to be seeded when building the binary var Version = "No version defined at build time" diff --git a/doc/commands/stolon-keeper.md b/doc/commands/stolon-keeper.md index a18311910..c46a47404 100644 --- a/doc/commands/stolon-keeper.md +++ b/doc/commands/stolon-keeper.md @@ -45,6 +45,7 @@ stolon-keeper [flags] --store-skip-tls-verify skip store certificate verification (insecure!!!) --store-timeout duration store request timeout (default 5s) --uid string keeper uid (must be unique in the cluster and can contain only lower-case letters, numbers and the underscore character). If not provided a random uid will be generated. + --wal-dir string wal directory ``` ###### Auto generated by spf13/cobra on 24-Feb-2021 diff --git a/doc/pitr.md b/doc/pitr.md index b974ebcfa..1be8782bf 100644 --- a/doc/pitr.md +++ b/doc/pitr.md @@ -39,7 +39,7 @@ Note: the `\"` is needed by json to put double quotes inside strings. We aren't When initializing a cluster in pitr init mode a random registered keeper will be choosed and it'll start restoring the database with these steps: * Remove the current data directory -* Call the `dataRestoreCommand` expanding every %d to the data directory full path. If it exits with a non zero exit code then stop here since something went wrong. +* Call the `dataRestoreCommand` expanding every %d to the data directory full path and every %w to the wal directory full path (if wal directory is provided to the keeper). If it exits with a non zero exit code then stop here since something went wrong. * Create a `recovery.conf` with the right parameters and with `restore_command` set to `restoreCommand`. * Start the postgres instance and wait for the archive recovery. diff --git a/doc/stolonctl.md b/doc/stolonctl.md index beb3d7353..28191cf88 100644 --- a/doc/stolonctl.md +++ b/doc/stolonctl.md @@ -33,7 +33,7 @@ $kubectl exec -i -t stolon-proxy-669f7b54fd-9psm2 -- stolonctl --cluster-name=ku Same `stolonctl` command as a one shot: ``` -kubectl run -i -t stolonctl --image=sorintlab/stolon:master-pg9.6 --restart=Never --rm -- /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap status +kubectl run -i -t stolonctl --image=ghcr.io/pgvillage-tools/stolon-keeper:17-latest --restart=Never --rm -- /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap status ``` ### See also diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md index c168df200..e67535ace 100644 --- a/examples/kubernetes/README.md +++ b/examples/kubernetes/README.md @@ -26,7 +26,7 @@ make PGVERSION=10 TAG=stolon:master-pg10 docker Once the image is built you should push it to the docker registry used by your kubernetes infrastructure. -The provided example uses `sorintlab/stolon:master-pg10` +The provided example uses `ghcr.io/pgvillage-tools/stolon-keeper:17-latest` ## Cluster setup and tests @@ -47,7 +47,7 @@ You can execute stolonctl in different ways: * as a one shot command executed inside a temporary pod: ``` -kubectl run -i -t stolonctl --image=sorintlab/stolon:master-pg10 --restart=Never --rm -- /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap init +kubectl run -i -t stolonctl --image=ghcr.io/pgvillage-tools/stolon-keeper:17-latest --restart=Never --rm -- /usr/local/bin/stolonctl --cluster-name=kube-stolon --store-backend=kubernetes --kube-resource-kind=configmap init ``` * from a machine that can access the store backend: diff --git a/examples/kubernetes/stolon-keeper.yaml b/examples/kubernetes/stolon-keeper.yaml index 6205fc4a1..49e72a613 100644 --- a/examples/kubernetes/stolon-keeper.yaml +++ b/examples/kubernetes/stolon-keeper.yaml @@ -25,7 +25,7 @@ spec: terminationGracePeriodSeconds: 10 containers: - name: stolon-keeper - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest command: - "/bin/bash" - "-ec" diff --git a/examples/kubernetes/stolon-proxy.yaml b/examples/kubernetes/stolon-proxy.yaml index d913e63c4..b299f7cdb 100644 --- a/examples/kubernetes/stolon-proxy.yaml +++ b/examples/kubernetes/stolon-proxy.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: stolon-proxy - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest command: - "/bin/bash" - "-ec" diff --git a/examples/kubernetes/stolon-sentinel.yaml b/examples/kubernetes/stolon-sentinel.yaml index 763c932ef..22a935723 100644 --- a/examples/kubernetes/stolon-sentinel.yaml +++ b/examples/kubernetes/stolon-sentinel.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: stolon-sentinel - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest command: - "/bin/bash" - "-ec" diff --git a/examples/swarm/README.md b/examples/swarm/README.md index 8c4c4400c..04212b8bd 100644 --- a/examples/swarm/README.md +++ b/examples/swarm/README.md @@ -26,7 +26,7 @@ make PGVERSION=10 TAG=stolon:master-pg10 docker Once the image is built you should push it to the docker registry used by your swarm infrastructure. -The provided example uses `sorintlab/stolon:master-pg10` +The provided example uses `ghcr.io/pgvillage-tools/stolon-keeper:17-latest` ## Cluster setup and tests diff --git a/examples/swarm/docker-compose-pg.yml b/examples/swarm/docker-compose-pg.yml index 2f27e23a8..6f89c48cf 100644 --- a/examples/swarm/docker-compose-pg.yml +++ b/examples/swarm/docker-compose-pg.yml @@ -8,7 +8,7 @@ secrets: services: sentinel: - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest command: gosu stolon stolon-sentinel --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level debug networks: - etcd_etcd @@ -22,7 +22,7 @@ services: failure_action: pause keeper1: - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest hostname: keeper1 environment: - PGDATA=/var/lib/postgresql/data @@ -41,7 +41,7 @@ services: # constraints: [node.labels.nodename == node1] keeper2: - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest hostname: keeper2 environment: - PGDATA=/var/lib/postgresql/data @@ -59,7 +59,7 @@ services: # constraints: [node.labels.nodename == node2] proxy: - image: sorintlab/stolon:master-pg10 + image: ghcr.io/pgvillage-tools/stolon-keeper:17-latest command: gosu stolon stolon-proxy --listen-address 0.0.0.0 --cluster-name stolon-cluster --store-backend=etcdv3 --store-endpoints http://etcd-00:2379,http://etcd-01:2379,http://etcd-02:2379 --log-level info networks: - etcd_etcd diff --git a/go.mod b/go.mod index b7a343098..d5c9d0e9b 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,168 @@ -module github.com/sorintlab/stolon +module github.com/pgvillage-tools/stolon require ( - github.com/coreos/bbolt v1.3.3 // indirect - github.com/coreos/etcd v3.3.18+incompatible // indirect - github.com/davecgh/go-spew v1.1.1 - github.com/docker/leadership v0.1.0 - github.com/docker/libkv v0.2.1 - github.com/evanphx/json-patch v4.5.0+incompatible - github.com/gofrs/uuid v4.2.0+incompatible - github.com/golang/mock v1.4.0 - github.com/google/go-cmp v0.4.0 - github.com/hashicorp/consul/api v1.4.0 - github.com/lib/pq v1.3.0 - github.com/mattn/go-isatty v0.0.12 - github.com/mitchellh/copystructure v1.0.0 - github.com/prometheus/client_golang v1.4.1 + github.com/Masterminds/semver/v3 v3.4.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/docker/go-connections v0.6.0 + github.com/evanphx/json-patch v5.9.0+incompatible + github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 + github.com/hashicorp/consul/api v1.30.0 + github.com/jackc/pgx/v5 v5.8.0 + github.com/kvtools/consul v1.0.2 + github.com/kvtools/etcdv2 v1.0.2 + github.com/kvtools/etcdv3 v1.0.3 + github.com/kvtools/valkeyrie v1.0.0 + github.com/lib/pq v1.10.9 + github.com/mattn/go-isatty v0.0.20 + github.com/mitchellh/copystructure v1.2.0 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 + github.com/prometheus/client_golang v1.20.5 + github.com/rs/zerolog v1.34.0 github.com/sgotti/gexpect v0.0.0-20210315095146-1ec64e69809b github.com/sorintlab/pollon v0.0.0-20181009091703-248c68238c16 - github.com/spf13/cobra v0.0.5 - github.com/spf13/pflag v1.0.5 - go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b - go.uber.org/zap v1.13.0 - k8s.io/api v0.17.3 - k8s.io/apimachinery v0.17.3 - k8s.io/client-go v0.17.3 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.9 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/etcd v0.40.0 + go.etcd.io/etcd/api/v3 v3.6.7 + go.etcd.io/etcd/client/v3 v3.6.7 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 ) -go 1.12 +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/armon/go-metrics v0.5.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 // 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/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sorintlab/tcpkeepalive v0.2.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect + go.etcd.io/etcd/client/v2 v2.305.26 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) -replace github.com/coreos/bbolt v1.3.3 => github.com/etcd-io/bbolt v1.3.3 +go 1.25.0 + +replace ( + github.com/armon/go-metrics => github.com/hashicorp/go-metrics v0.4.1 + github.com/imdario/mergo => dario.cat/mergo v0.3.16 +) diff --git a/go.sum b/go.sum index f16a8ba23..8d6cf3601 100644 --- a/go.sum +++ b/go.sum @@ -1,505 +1,579 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.18+incompatible h1:Zz1aXgDrFFi1nadh58tA9ktt06cmPTwNNP3dXwIq1lE= -github.com/coreos/etcd v3.3.18+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +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/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/docker/leadership v0.1.0 h1:od6W3af8ONBDRFnOJb0JghHx8rHggGHaTYGTIwYMo/k= -github.com/docker/leadership v0.1.0/go.mod h1:6yL2hg00l43fYEJagcF7eIS4PootU7TAO122H8bUUAo= -github.com/docker/libkv v0.2.1 h1:PNXYaftMVCFS5CmnDtDWTg3wbBO61Q/cEo3KX1oKxto= -github.com/docker/libkv v0.2.1/go.mod h1:r5hEwHwW8dr0TFBYGCarMNbrQOiwL1xoqDYZ/JqoTK0= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= -github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= -github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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 v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 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.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/go-cmp v0.5.6/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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= -github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.4.0 h1:jfESivXnO5uLdH650JU/6AnjRoHrLhULq0FnC3Kp9EY= -github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU= -github.com/hashicorp/consul/sdk v0.4.0 h1:zBtCfKJZcJDBvSCkQJch4ulp59m1rATFLKwNo/LYY30= -github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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.27.8 h1:NpbJl/eVbvrGE0MJ6X16X9SAifesl6Fwxg/YmCvubRI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8/go.mod h1:mi7YA+gCzVem12exXy46ZespvGtX/lZmD/RLnQhVW7U= +github.com/hashicorp/consul/api v1.30.0 h1:ArHVMMILb1nQv8vZSGIwwQd2gtc+oSQZ6CalyiyH2XQ= +github.com/hashicorp/consul/api v1.30.0/go.mod h1:B2uGchvaXVW2JhFoS8nqTxMD5PBykr4ebY4JWHTTeLM= +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/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +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-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +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.4.1 h1:3CTMzft9hdMnx2CKrEPjJqwdMAN/ij7bjqzNo+JpVZ4= +github.com/hashicorp/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +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 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= 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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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 v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kvtools/consul v1.0.2 h1:ltPgs4Ld09Xaa7zrOJ/TewBYKAsr11/LRFpErdkb8AA= +github.com/kvtools/consul v1.0.2/go.mod h1:bFnzfGJ5ZIRRXCBGBmwhJlLdEWOlrjOcS1WjyAQzaJA= +github.com/kvtools/etcdv2 v1.0.2 h1:Bigfbm2f4uZveD0Av/5Vxs7MdqnWDVnAFpSF4QEL1c8= +github.com/kvtools/etcdv2 v1.0.2/go.mod h1:Ye5IwvG5KxUdcP14Yag6Pc3E9bzxy1j7zlvLK8eI/bU= +github.com/kvtools/etcdv3 v1.0.3 h1:bsaGf8Jsi8Xq6h/KVV/D7F/c1IuVQv2f7tuVxeA//fk= +github.com/kvtools/etcdv3 v1.0.3/go.mod h1:ID4AIRgCuCRzzdITo9O5RKUtLwfu/zJvCvosrFcBK4U= +github.com/kvtools/valkeyrie v1.0.0 h1:LAITop2wPoYCMitR24GZZsW0b57hmI+ePD18VRTtOf0= +github.com/kvtools/valkeyrie v1.0.0/go.mod h1:bDi/OdhJCSbGPMsCgUQl881yuEweKCSItAtTBI+ZjpU= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 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.4/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.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 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/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= -github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 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 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 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/sgotti/gexpect v0.0.0-20210315095146-1ec64e69809b h1:rGT0mqolw5UvjfByF0vWfFEhtL7Hn6P7dNKz7iHBMdA= github.com/sgotti/gexpect v0.0.0-20210315095146-1ec64e69809b/go.mod h1:iw90eoXMZYeYjqjjf85v5Soj3BXaF4hXI0SbESk5kTw= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sorintlab/pollon v0.0.0-20181009091703-248c68238c16 h1:qOg8jM4RzD8CysFs4El1l7hwtl4bLApqbhh7EoIxLns= github.com/sorintlab/pollon v0.0.0-20181009091703-248c68238c16/go.mod h1:5vFcIguA+p40loRzZZejq5PdOVLz86bwSgeN8kRffIE= github.com/sorintlab/tcpkeepalive v0.2.0 h1:qHYOzlXMtGIhIUIaDFS1KdygDPSnJZWE6kxI9/abvG4= github.com/sorintlab/tcpkeepalive v0.2.0/go.mod h1:99dB6dRb+0HML/17LgJalvzJ5FsAfC5ryQX2x2x8yw8= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b h1:5makfKENOTVu2bNoHzSqwwz+g70ivWLSnExzd33/2bI= -go.etcd.io/etcd v0.0.0-20201125193152-8a03d2e9614b/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/etcd v0.40.0 h1:9uZrotowD6Z9qgpd8w46UXi1x5bkhOcpveK5rvWy5u0= +github.com/testcontainers/testcontainers-go/modules/etcd v0.40.0/go.mod h1:z5saei5a/cpuXYz3MJqJ91RMBYOqw7OXDueN8XKoALA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0= +go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI= +go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs= +go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q= +go.etcd.io/etcd/client/v2 v2.305.26 h1:oReO+h1y3W/CJJa8axZ/3t9S6jg0I42tNx7AWKIvfCc= +go.etcd.io/etcd/client/v2 v2.305.26/go.mod h1:oni2jI2OMezwmakWDTlZRi6VANemxGx+KIuiQBuRRpQ= +go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U= +go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-20190227155943-e225da77a7e6/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 h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210314195730-07df6a141424 h1:+39ahH47SWi1PhMRAHfIrm8f69HRZ5K2koXH6dmO8TQ= +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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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-20220715151400-c0bba94af5f8/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +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= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= -k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= -k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= -k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= -k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU= -k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= -k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go deleted file mode 100644 index 194d6635a..000000000 --- a/internal/cluster/cluster.go +++ /dev/null @@ -1,784 +0,0 @@ -// Copyright 2016 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package cluster - -import ( - "encoding/json" - "fmt" - "reflect" - "sort" - "strings" - "time" - - "github.com/mitchellh/copystructure" - "github.com/sorintlab/stolon/internal/common" - util "github.com/sorintlab/stolon/internal/postgresql" -) - -func Uint16P(u uint16) *uint16 { - return &u -} -func Uint32P(u uint32) *uint32 { - return &u -} - -func BoolP(b bool) *bool { - return &b -} - -const ( - CurrentCDFormatVersion uint64 = 1 -) - -const ( - DefaultStoreTimeout = 5 * time.Second - - DefaultDBNotIncreasingXLogPosTimes = 10 - - DefaultSleepInterval = 5 * time.Second - DefaultRequestTimeout = 10 * time.Second - DefaultConvergenceTimeout = 30 * time.Second - DefaultInitTimeout = 5 * time.Minute - DefaultSyncTimeout = 0 - DefaultDBWaitReadyTimeout = 60 * time.Second - DefaultFailInterval = 20 * time.Second - DefaultDeadKeeperRemovalInterval = 48 * time.Hour - DefaultProxyCheckInterval = 5 * time.Second - DefaultProxyTimeout = 15 * time.Second - DefaultMaxStandbys uint16 = 20 - DefaultMaxStandbysPerSender uint16 = 3 - DefaultMaxStandbyLag = 1024 * 1204 - DefaultSynchronousReplication = false - DefaultMinSynchronousStandbys uint16 = 1 - DefaultMaxSynchronousStandbys uint16 = 1 - DefaultAdditionalWalSenders = 5 - DefaultUsePgrewind = false - DefaultMergePGParameter = true - DefaultRole ClusterRole = ClusterRoleMaster - DefaultSUReplAccess SUReplAccessMode = SUReplAccessAll - DefaultAutomaticPgRestart = false -) - -const ( - NoGeneration int64 = 0 - InitialGeneration int64 = 1 -) - -type PGParameters map[string]string - -type FollowType string - -const ( - // Follow an db managed by a keeper in our cluster - FollowTypeInternal FollowType = "internal" - // Follow an external db - FollowTypeExternal FollowType = "external" -) - -type FollowConfig struct { - Type FollowType `json:"type,omitempty"` - // Keeper ID to follow when Type is "internal" - DBUID string `json:"dbuid,omitempty"` - // Standby settings when Type is "external" - StandbySettings *StandbySettings `json:"standbySettings,omitempty"` - ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` -} - -type PostgresBinaryVersion struct { - Maj int - Min int -} - -type ClusterPhase string - -const ( - ClusterPhaseInitializing ClusterPhase = "initializing" - ClusterPhaseNormal ClusterPhase = "normal" -) - -type ClusterRole string - -const ( - ClusterRoleMaster ClusterRole = "master" - ClusterRoleStandby ClusterRole = "standby" -) - -type ClusterInitMode string - -const ( - // Initialize a cluster starting from a freshly initialized database cluster. Valid only when cluster role is master. - ClusterInitModeNew ClusterInitMode = "new" - // Initialize a cluster doing a point in time recovery on a keeper. - ClusterInitModePITR ClusterInitMode = "pitr" - // Initialize a cluster with an user specified already populated db cluster. - ClusterInitModeExisting ClusterInitMode = "existing" -) - -func ClusterInitModeP(s ClusterInitMode) *ClusterInitMode { - return &s -} - -func ClusterRoleP(s ClusterRole) *ClusterRole { - return &s -} - -type DBInitMode string - -const ( - DBInitModeNone DBInitMode = "none" - // Use existing db cluster data - DBInitModeExisting DBInitMode = "existing" - // Initialize a db starting from a freshly initialized database cluster - DBInitModeNew DBInitMode = "new" - // Initialize a db doing a point in time recovery - DBInitModePITR DBInitMode = "pitr" - // Initialize a db doing a resync to a target database cluster - DBInitModeResync DBInitMode = "resync" -) - -type NewConfig struct { - Locale string `json:"locale,omitempty"` - Encoding string `json:"encoding,omitempty"` - DataChecksums bool `json:"dataChecksums,omitempty"` -} - -type PITRConfig struct { - // DataRestoreCommand defines the command to execute for restoring the db - // cluster data). %d is replaced with the full path to the db cluster - // datadir. Use %% to embed an actual % character. - DataRestoreCommand string `json:"dataRestoreCommand,omitempty"` - ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` - RecoveryTargetSettings *RecoveryTargetSettings `json:"recoveryTargetSettings,omitempty"` -} - -type ExistingConfig struct { - KeeperUID string `json:"keeperUID,omitempty"` -} - -// Standby config when role is standby -type StandbyConfig struct { - StandbySettings *StandbySettings `json:"standbySettings,omitempty"` - ArchiveRecoverySettings *ArchiveRecoverySettings `json:"archiveRecoverySettings,omitempty"` -} - -// ArchiveRecoverySettings defines the archive recovery settings in the recovery.conf file (https://www.postgresql.org/docs/9.6/static/archive-recovery-settings.html ) -type ArchiveRecoverySettings struct { - // value for restore_command - RestoreCommand string `json:"restoreCommand,omitempty"` -} - -// RecoveryTargetSettings defines the recovery target settings in the recovery.conf file (https://www.postgresql.org/docs/9.6/static/recovery-target-settings.html ) -type RecoveryTargetSettings struct { - RecoveryTarget string `json:"recoveryTarget,omitempty"` - RecoveryTargetLsn string `json:"recoveryTargetLsn,omitempty"` - RecoveryTargetName string `json:"recoveryTargetName,omitempty"` - RecoveryTargetTime string `json:"recoveryTargetTime,omitempty"` - RecoveryTargetXid string `json:"recoveryTargetXid,omitempty"` - RecoveryTargetTimeline string `json:"recoveryTargetTimeline,omitempty"` -} - -// StandbySettings defines the standby settings in the recovery.conf file (https://www.postgresql.org/docs/9.6/static/standby-settings.html ) -type StandbySettings struct { - PrimaryConninfo string `json:"primaryConninfo,omitempty"` - PrimarySlotName string `json:"primarySlotName,omitempty"` - RecoveryMinApplyDelay string `json:"recoveryMinApplyDelay,omitempty"` -} - -type SUReplAccessMode string - -const ( - // Allow access from every host - SUReplAccessAll SUReplAccessMode = "all" - // Allow access from standby server IPs only - SUReplAccessStrict SUReplAccessMode = "strict" -) - -func SUReplAccessModeP(s SUReplAccessMode) *SUReplAccessMode { - return &s -} - -type ClusterSpec struct { - // Interval to wait before next check - SleepInterval *Duration `json:"sleepInterval,omitempty"` - // Time after which any request (keepers checks from sentinel etc...) will fail. - RequestTimeout *Duration `json:"requestTimeout,omitempty"` - // Interval to wait for a db to be converged to the required state when - // no long operation are expected. - ConvergenceTimeout *Duration `json:"convergenceTimeout,omitempty"` - // Interval to wait for a db to be initialized (doing a initdb) - InitTimeout *Duration `json:"initTimeout,omitempty"` - // Interval to wait for a db to be synced with a master - SyncTimeout *Duration `json:"syncTimeout,omitempty"` - // Interval to wait for a db to boot and become ready - DBWaitReadyTimeout *Duration `json:"dbWaitReadyTimeout,omitempty"` - // Interval after the first fail to declare a keeper or a db as not healthy. - FailInterval *Duration `json:"failInterval,omitempty"` - // Interval after which a dead keeper will be removed from the cluster data - DeadKeeperRemovalInterval *Duration `json:"deadKeeperRemovalInterval,omitempty"` - // Interval to wait before next proxy check - ProxyCheckInterval *Duration `json:"proxyCheckInterval,omitempty"` - // Interval where the proxy must successfully complete a check - ProxyTimeout *Duration `json:"proxyTimeout,omitempty"` - // Max number of standbys. This needs to be greater enough to cover both - // standby managed by stolon and additional standbys configured by the - // user. Its value affect different postgres parameters like - // max_replication_slots and max_wal_senders. Setting this to a number - // lower than the sum of stolon managed standbys and user managed - // standbys will have unpredicatable effects due to problems creating - // replication slots or replication problems due to exhausted wal - // senders. - MaxStandbys *uint16 `json:"maxStandbys,omitempty"` - // Max number of standbys for every sender. A sender can be a master or - // another standby (if/when implementing cascading replication). - MaxStandbysPerSender *uint16 `json:"maxStandbysPerSender,omitempty"` - // Max lag in bytes that an asynchronous standy can have to be elected in - // place of a failed master - MaxStandbyLag *uint32 `json:"maxStandbyLag,omitempty"` - // Use Synchronous replication between master and its standbys - SynchronousReplication *bool `json:"synchronousReplication,omitempty"` - // MinSynchronousStandbys is the mininum number if synchronous standbys - // to be configured when SynchronousReplication is true - MinSynchronousStandbys *uint16 `json:"minSynchronousStandbys,omitempty"` - // MaxSynchronousStandbys is the maximum number if synchronous standbys - // to be configured when SynchronousReplication is true - MaxSynchronousStandbys *uint16 `json:"maxSynchronousStandbys,omitempty"` - // AdditionalWalSenders defines the number of additional wal_senders in - // addition to the ones internally defined by stolon - AdditionalWalSenders *uint16 `json:"additionalWalSenders"` - // AdditionalMasterReplicationSlots defines additional replication slots to - // be created on the master postgres instance. Replication slots not defined - // here will be dropped from the master instance (i.e. manually created - // replication slots will be removed). - AdditionalMasterReplicationSlots []string `json:"additionalMasterReplicationSlots"` - // Whether to use pg_rewind - UsePgrewind *bool `json:"usePgrewind,omitempty"` - // InitMode defines the cluster initialization mode. Current modes are: new, existing, pitr - InitMode *ClusterInitMode `json:"initMode,omitempty"` - // Whether to merge pgParameters of the initialized db cluster, useful - // the retain initdb generated parameters when InitMode is new, retain - // current parameters when initMode is existing or pitr. - MergePgParameters *bool `json:"mergePgParameters,omitempty"` - // Role defines the cluster operating role (master or standby of an external database) - Role *ClusterRole `json:"role,omitempty"` - // Init configuration used when InitMode is "new" - NewConfig *NewConfig `json:"newConfig,omitempty"` - // Point in time recovery init configuration used when InitMode is "pitr" - PITRConfig *PITRConfig `json:"pitrConfig,omitempty"` - // Existing init configuration used when InitMode is "existing" - ExistingConfig *ExistingConfig `json:"existingConfig,omitempty"` - // Standby config when role is standby - StandbyConfig *StandbyConfig `json:"standbyConfig,omitempty"` - // Define the mode of the default hba rules needed for replication by standby keepers (the su and repl auth methods will be the one provided in the keeper command line options) - // Values can be "all" or "strict", "all" allow access from all ips, "strict" restrict master access to standby servers ips. - // Default is "all" - DefaultSUReplAccessMode *SUReplAccessMode `json:"defaultSUReplAccessMode,omitempty"` - // Map of postgres parameters - PGParameters PGParameters `json:"pgParameters,omitempty"` - // Additional pg_hba.conf entries - // we don't set omitempty since we want to distinguish between null or empty slice - PGHBA []string `json:"pgHBA"` - // Enable automatic pg restart when pg parameters that requires restart changes - AutomaticPgRestart *bool `json:"automaticPgRestart"` -} - -type ClusterStatus struct { - CurrentGeneration int64 `json:"currentGeneration,omitempty"` - Phase ClusterPhase `json:"phase,omitempty"` - // Master DB UID - Master string `json:"master,omitempty"` -} - -type Cluster struct { - UID string `json:"uid,omitempty"` - Generation int64 `json:"generation,omitempty"` - ChangeTime time.Time `json:"changeTime,omitempty"` - - Spec *ClusterSpec `json:"spec,omitempty"` - - Status ClusterStatus `json:"status,omitempty"` -} - -func (c *Cluster) DeepCopy() *Cluster { - nc, err := copystructure.Copy(c) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(c, nc) { - panic("not equal") - } - return nc.(*Cluster) -} - -func (c *ClusterSpec) DeepCopy() *ClusterSpec { - nc, err := copystructure.Copy(c) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(c, nc) { - panic("not equal") - } - return nc.(*ClusterSpec) -} - -// DefSpec returns a new ClusterSpec with unspecified values populated with -// their defaults -func (c *Cluster) DefSpec() *ClusterSpec { - return c.Spec.WithDefaults() -} - -// WithDefaults returns a new ClusterSpec with unspecified values populated with -// their defaults -func (os *ClusterSpec) WithDefaults() *ClusterSpec { - // Take a copy of the input ClusterSpec since we don't want to change the original - s := os.DeepCopy() - if s.SleepInterval == nil { - s.SleepInterval = &Duration{Duration: DefaultSleepInterval} - } - if s.RequestTimeout == nil { - s.RequestTimeout = &Duration{Duration: DefaultRequestTimeout} - } - if s.ConvergenceTimeout == nil { - s.ConvergenceTimeout = &Duration{Duration: DefaultConvergenceTimeout} - } - if s.InitTimeout == nil { - s.InitTimeout = &Duration{Duration: DefaultInitTimeout} - } - if s.SyncTimeout == nil { - s.SyncTimeout = &Duration{Duration: DefaultSyncTimeout} - } - if s.DBWaitReadyTimeout == nil { - s.DBWaitReadyTimeout = &Duration{Duration: DefaultDBWaitReadyTimeout} - } - if s.FailInterval == nil { - s.FailInterval = &Duration{Duration: DefaultFailInterval} - } - if s.DeadKeeperRemovalInterval == nil { - s.DeadKeeperRemovalInterval = &Duration{Duration: DefaultDeadKeeperRemovalInterval} - } - if s.ProxyCheckInterval == nil { - s.ProxyCheckInterval = &Duration{Duration: DefaultProxyCheckInterval} - } - if s.ProxyTimeout == nil { - s.ProxyTimeout = &Duration{Duration: DefaultProxyTimeout} - } - if s.MaxStandbys == nil { - s.MaxStandbys = Uint16P(DefaultMaxStandbys) - } - if s.MaxStandbysPerSender == nil { - s.MaxStandbysPerSender = Uint16P(DefaultMaxStandbysPerSender) - } - if s.MaxStandbyLag == nil { - s.MaxStandbyLag = Uint32P(DefaultMaxStandbyLag) - } - if s.SynchronousReplication == nil { - s.SynchronousReplication = BoolP(DefaultSynchronousReplication) - } - if s.UsePgrewind == nil { - s.UsePgrewind = BoolP(DefaultUsePgrewind) - } - if s.MinSynchronousStandbys == nil { - s.MinSynchronousStandbys = Uint16P(DefaultMinSynchronousStandbys) - } - if s.MaxSynchronousStandbys == nil { - s.MaxSynchronousStandbys = Uint16P(DefaultMaxSynchronousStandbys) - } - if s.AdditionalWalSenders == nil { - s.AdditionalWalSenders = Uint16P(DefaultAdditionalWalSenders) - } - if s.MergePgParameters == nil { - s.MergePgParameters = BoolP(DefaultMergePGParameter) - } - if s.DefaultSUReplAccessMode == nil { - v := DefaultSUReplAccess - s.DefaultSUReplAccessMode = &v - } - if s.Role == nil { - v := DefaultRole - s.Role = &v - } - if s.AutomaticPgRestart == nil { - s.AutomaticPgRestart = BoolP(DefaultAutomaticPgRestart) - } - return s -} - -// Validate validates a cluster spec. -func (os *ClusterSpec) Validate() error { - s := os.WithDefaults() - if s.SleepInterval.Duration < 0 { - return fmt.Errorf("sleepInterval must be positive") - } - if s.RequestTimeout.Duration < 0 { - return fmt.Errorf("requestTimeout must be positive") - } - if s.ConvergenceTimeout.Duration < 0 { - return fmt.Errorf("convergenceTimeout must be positive") - } - if s.InitTimeout.Duration < 0 { - return fmt.Errorf("initTimeout must be positive") - } - if s.SyncTimeout.Duration < 0 { - return fmt.Errorf("syncTimeout must be positive") - } - if s.DBWaitReadyTimeout.Duration < 0 { - return fmt.Errorf("dbWaitReadyTimeout must be positive") - } - if s.FailInterval.Duration < 0 { - return fmt.Errorf("failInterval must be positive") - } - if s.DeadKeeperRemovalInterval.Duration < 0 { - return fmt.Errorf("deadKeeperRemovalInterval must be positive") - } - if s.ProxyCheckInterval.Duration < 0 { - return fmt.Errorf("proxyCheckInterval must be positive") - } - if s.ProxyTimeout.Duration < 0 { - return fmt.Errorf("proxyTimeout must be positive") - } - if s.ProxyCheckInterval.Duration >= s.ProxyTimeout.Duration { - return fmt.Errorf("proxyCheckInterval should be less than proxyTimeout") - } - if *s.MaxStandbys < 1 { - return fmt.Errorf("maxStandbys must be at least 1") - } - if *s.MaxStandbysPerSender < 1 { - return fmt.Errorf("maxStandbysPerSender must be at least 1") - } - if *s.MaxSynchronousStandbys < 1 { - return fmt.Errorf("maxSynchronousStandbys must be at least 1") - } - if *s.MaxSynchronousStandbys < *s.MinSynchronousStandbys { - return fmt.Errorf("maxSynchronousStandbys must be greater or equal to minSynchronousStandbys") - } - if s.InitMode == nil { - return fmt.Errorf("initMode undefined") - } - for _, replicationSlot := range s.AdditionalMasterReplicationSlots { - if err := validateReplicationSlot(replicationSlot); err != nil { - return err - } - } - - // The unique validation we're doing on pgHBA entries is that they don't contain a newline character - for _, e := range s.PGHBA { - if strings.Contains(e, "\n") { - return fmt.Errorf("pgHBA entries cannot contain newline characters") - } - } - - switch *s.InitMode { - case ClusterInitModeNew: - if *s.Role == ClusterRoleStandby { - return fmt.Errorf("invalid cluster role standby when initMode is \"new\"") - } - case ClusterInitModeExisting: - if s.ExistingConfig == nil { - return fmt.Errorf("existingConfig undefined. Required when initMode is \"existing\"") - } - if s.ExistingConfig.KeeperUID == "" { - return fmt.Errorf("existingConfig.keeperUID undefined") - } - case ClusterInitModePITR: - if s.PITRConfig == nil { - return fmt.Errorf("pitrConfig undefined. Required when initMode is \"pitr\"") - } - if s.PITRConfig.DataRestoreCommand == "" { - return fmt.Errorf("pitrConfig.DataRestoreCommand undefined") - } - if s.PITRConfig.RecoveryTargetSettings != nil && *s.Role == ClusterRoleStandby { - return fmt.Errorf("cannot define pitrConfig.RecoveryTargetSettings when required cluster role is standby") - } - default: - return fmt.Errorf("unknown initMode: %q", *s.InitMode) - - } - - switch *s.DefaultSUReplAccessMode { - case SUReplAccessAll: - case SUReplAccessStrict: - default: - return fmt.Errorf("unknown defaultSUReplAccessMode: %q", *s.DefaultSUReplAccessMode) - } - - switch *s.Role { - case ClusterRoleMaster: - case ClusterRoleStandby: - if s.StandbyConfig == nil { - return fmt.Errorf("standbyConfig undefined. Required when cluster role is \"standby\"") - } - default: - return fmt.Errorf("unknown role: %q", *s.InitMode) - } - return nil -} - -func validateReplicationSlot(replicationSlot string) error { - if !util.IsValidReplSlotName(replicationSlot) { - return fmt.Errorf("wrong replication slot name: %q", replicationSlot) - } - if common.IsStolonName(replicationSlot) { - return fmt.Errorf("replication slot name is reserved: %q", replicationSlot) - } - return nil -} - -func (c *Cluster) UpdateSpec(ns *ClusterSpec) error { - s := c.Spec - if err := ns.Validate(); err != nil { - return fmt.Errorf("invalid cluster spec: %v", err) - } - ds := s.WithDefaults() - dns := ns.WithDefaults() - if *ds.InitMode != *dns.InitMode { - return fmt.Errorf("cannot change cluster init mode") - } - if *ds.Role == ClusterRoleMaster && *dns.Role == ClusterRoleStandby { - return fmt.Errorf("cannot update a cluster from master role to standby role") - } - c.Spec = ns - return nil -} - -func NewCluster(uid string, cs *ClusterSpec) *Cluster { - c := &Cluster{ - UID: uid, - Generation: InitialGeneration, - ChangeTime: time.Now(), - Spec: cs, - Status: ClusterStatus{ - Phase: ClusterPhaseInitializing, - }, - } - return c -} - -type KeeperSpec struct{} - -type KeeperStatus struct { - Healthy bool `json:"healthy,omitempty"` - LastHealthyTime time.Time `json:"lastHealthyTime,omitempty"` - - BootUUID string `json:"bootUUID,omitempty"` - - PostgresBinaryVersion PostgresBinaryVersion `json:"postgresBinaryVersion,omitempty"` - - ForceFail bool `json:"forceFail,omitempty"` - - CanBeMaster *bool `json:"canBeMaster,omitempty"` - CanBeSynchronousReplica *bool `json:"canBeSynchronousReplica,omitempty"` -} - -type Keeper struct { - // Keeper ID - UID string `json:"uid,omitempty"` - Generation int64 `json:"generation,omitempty"` - ChangeTime time.Time `json:"changeTime,omitempty"` - - Spec *KeeperSpec `json:"spec,omitempty"` - - Status KeeperStatus `json:"status,omitempty"` -} - -func NewKeeperFromKeeperInfo(ki *KeeperInfo) *Keeper { - return &Keeper{ - UID: ki.UID, - Generation: InitialGeneration, - ChangeTime: time.Time{}, - Spec: &KeeperSpec{}, - Status: KeeperStatus{ - Healthy: true, - LastHealthyTime: time.Now(), - BootUUID: ki.BootUUID, - }, - } -} - -func (kss Keepers) SortedKeys() []string { - keys := []string{} - for k := range kss { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -type DBSpec struct { - // The KeeperUID this db is assigned to - KeeperUID string `json:"keeperUID,omitempty"` - // Time after which any request (keepers checks from sentinel etc...) will fail. - RequestTimeout Duration `json:"requestTimeout,omitempty"` - // See ClusterSpec MaxStandbys description - MaxStandbys uint16 `json:"maxStandbys,omitempty"` - // Use Synchronous replication between master and its standbys - SynchronousReplication bool `json:"synchronousReplication,omitempty"` - // Whether to use pg_rewind - UsePgrewind bool `json:"usePgrewind,omitempty"` - // AdditionalWalSenders defines the number of additional wal_senders in - // addition to the ones internally defined by stolon - AdditionalWalSenders uint16 `json:"additionalWalSenders"` - // AdditionalReplicationSlots is a list of additional replication slots. - // Replication slots not defined here will be dropped from the instance - // (i.e. manually created replication slots will be removed). - AdditionalReplicationSlots []string `json:"additionalReplicationSlots"` - // InitMode defines the db initialization mode. Current modes are: none, new - InitMode DBInitMode `json:"initMode,omitempty"` - // Init configuration used when InitMode is "new" - NewConfig *NewConfig `json:"newConfig,omitempty"` - // Point in time recovery init configuration used when InitMode is "pitr" - PITRConfig *PITRConfig `json:"pitrConfig,omitempty"` - // Map of postgres parameters - PGParameters PGParameters `json:"pgParameters,omitempty"` - // Additional pg_hba.conf entries - // We don't set omitempty since we want to distinguish between null or empty slice - PGHBA []string `json:"pgHBA"` - // DB Role (master or standby) - Role common.Role `json:"role,omitempty"` - // FollowConfig when Role is "standby" - FollowConfig *FollowConfig `json:"followConfig,omitempty"` - // Followers DB UIDs - Followers []string `json:"followers"` - // Whether to include previous postgresql.conf - IncludeConfig bool `json:"includePreviousConfig,omitempty"` - // SynchronousStandbys are the standbys to be configured as synchronous - SynchronousStandbys []string `json:"synchronousStandbys"` - // External SynchronousStandbys are external standbys names to be configured as synchronous - ExternalSynchronousStandbys []string `json:"externalSynchronousStandbys"` -} - -type DBStatus struct { - Healthy bool `json:"healthy,omitempty"` - - CurrentGeneration int64 `json:"currentGeneration,omitempty"` - - ListenAddress string `json:"listenAddress,omitempty"` - Port string `json:"port,omitempty"` - - SystemID string `json:"systemdID,omitempty"` - TimelineID uint64 `json:"timelineID,omitempty"` - XLogPos uint64 `json:"xLogPos,omitempty"` - TimelinesHistory PostgresTimelinesHistory `json:"timelinesHistory,omitempty"` - - PGParameters PGParameters `json:"pgParameters,omitempty"` - - // DBUIDs of the internal standbys currently reported as in sync by the instance - CurSynchronousStandbys []string `json:"-"` - - // DBUIDs of the internal standbys that we know are in sync. - // They could be currently down but we know that they were reported as in - // sync in the past and they are defined inside synchronous_standby_names - // so the instance will wait for acknowledge from them. - SynchronousStandbys []string `json:"synchronousStandbys"` - - // NOTE(sgotti) we currently don't report the external synchronous standbys. - // If/when needed lets add a new ExternalSynchronousStandbys field - - OlderWalFile string `json:"olderWalFile,omitempty"` -} - -type DB struct { - UID string `json:"uid,omitempty"` - Generation int64 `json:"generation,omitempty"` - ChangeTime time.Time `json:"changeTime,omitempty"` - - Spec *DBSpec `json:"spec,omitempty"` - - Status DBStatus `json:"status,omitempty"` -} - -type ProxySpec struct { - MasterDBUID string `json:"masterDbUid,omitempty"` - EnabledProxies []string `json:"enabledProxies,omitempty"` -} - -type ProxyStatus struct { -} - -type Proxy struct { - UID string `json:"uid,omitempty"` - Generation int64 `json:"generation,omitempty"` - ChangeTime time.Time `json:"changeTime,omitempty"` - - Spec ProxySpec `json:"spec,omitempty"` - - Status ProxyStatus `json:"status,omitempty"` -} - -// Duration is needed to be able to marshal/unmarshal json strings with time -// unit (eg. 3s, 100ms) instead of ugly times in nanoseconds. -type Duration struct { - time.Duration -} - -func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(d.String()) -} - -func (d *Duration) UnmarshalJSON(b []byte) error { - s := strings.Trim(string(b), `"`) - du, err := time.ParseDuration(s) - if err != nil { - return err - } - d.Duration = du - return nil -} - -type Keepers map[string]*Keeper -type DBs map[string]*DB - -// for simplicity keep all the changes to the various components atomic (using -// an unique key) -type ClusterData struct { - // ClusterData format version. Used to detect incompatible - // version and do upgrade. Needs to be bumped when a non - // backward compatible change is done to the other struct - // members. - FormatVersion uint64 `json:"formatVersion"` - ChangeTime time.Time `json:"changeTime"` - Cluster *Cluster `json:"cluster"` - Keepers Keepers `json:"keepers"` - DBs DBs `json:"dbs"` - Proxy *Proxy `json:"proxy"` -} - -func NewClusterData(c *Cluster) *ClusterData { - return &ClusterData{ - FormatVersion: CurrentCDFormatVersion, - Cluster: c, - Keepers: make(Keepers), - DBs: make(DBs), - Proxy: &Proxy{}, - } -} - -func (c *ClusterData) DeepCopy() *ClusterData { - nc, err := copystructure.Copy(c) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(c, nc) { - panic("not equal") - } - return nc.(*ClusterData) -} - -func (cd *ClusterData) FindDB(keeper *Keeper) *DB { - for _, db := range cd.DBs { - if db.Spec.KeeperUID == keeper.UID { - return db - } - } - return nil -} diff --git a/internal/cluster/cluster_test.go b/internal/cluster/cluster_test.go deleted file mode 100644 index 2fbde7645..000000000 --- a/internal/cluster/cluster_test.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package cluster - -import ( - "errors" - "testing" -) - -func TestValidateReplicationSlots(t *testing.T) { - tests := []struct { - in string - err error - }{ - { - in: "goodslotname_434432", - }, - { - in: "badslotname-34223", - err: errors.New(`wrong replication slot name: "badslotname-34223"`), - }, - { - in: "badslotname\n", - err: errors.New(`wrong replication slot name: "badslotname\n"`), - }, - { - in: " badslotname", - err: errors.New(`wrong replication slot name: " badslotname"`), - }, - { - in: "badslotname ", - err: errors.New(`wrong replication slot name: "badslotname "`), - }, - { - in: "stolon_c874a3cb", - err: errors.New(`replication slot name is reserved: "stolon_c874a3cb"`), - }, - } - - for i, tt := range tests { - err := validateReplicationSlot(tt.in) - - if tt.err != nil { - if err == nil { - t.Errorf("#%d: got no error, wanted error: %v", i, tt.err) - } else if tt.err.Error() != err.Error() { - t.Errorf("#%d: got error: %v, wanted error: %v", i, err, tt.err) - } - } else { - if err != nil { - t.Errorf("#%d: unexpected error: %v", i, err) - } - } - } -} - -func TestClusterData_FindDB(t *testing.T) { - db := DB{ - UID: "dbUUID", - Spec: &DBSpec{KeeperUID: "sameKeeperUUID"}, - } - tests := []struct { - name string - clusterData ClusterData - keeper *Keeper - expectedDB *DB - }{ - { - name: "should return nil if the clusterData is empty", - clusterData: ClusterData{}, - keeper: &Keeper{}, - expectedDB: nil, - }, - { - name: "should return nil if DB is not found for given keeper", - clusterData: ClusterData{ - DBs: map[string]*DB{ - "dbUUID": &db, - }, - }, - keeper: &Keeper{UID: "differentUUID"}, - expectedDB: nil, - }, - { - name: "should return the DB if DB is found for given keeper", - clusterData: ClusterData{ - DBs: map[string]*DB{ - "dbUUID": &db, - }, - }, - keeper: &Keeper{UID: "sameKeeperUUID"}, - expectedDB: &db, - }, - } - - for _, tt := range tests { - actual := tt.clusterData.FindDB(tt.keeper) - if actual != tt.expectedDB { - t.Errorf("Expected %v, but got %v", tt.expectedDB, actual) - } - } -} diff --git a/internal/cluster/v0/config_test.go b/internal/cluster/v0/config_test.go deleted file mode 100644 index f7bb0b122..000000000 --- a/internal/cluster/v0/config_test.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2015 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package v0 - -import ( - "encoding/json" - "fmt" - "reflect" - "testing" - "time" - - "github.com/davecgh/go-spew/spew" -) - -func TestParseConfig(t *testing.T) { - tests := []struct { - in string - cfg *Config - err error - }{ - { - in: "{}", - cfg: mergeDefaults(&NilConfig{}).ToConfig(), - err: nil, - }, - // Test duration parsing - { - in: `{ "request_timeout": "3s" }`, - cfg: mergeDefaults(&NilConfig{RequestTimeout: &Duration{3 * time.Second}}).ToConfig(), - err: nil, - }, - { - in: `{ "request_timeout": "3000ms" }`, - cfg: mergeDefaults(&NilConfig{RequestTimeout: &Duration{3 * time.Second}}).ToConfig(), - err: nil, - }, - { - in: `{ "request_timeout": "-3s" }`, - cfg: nil, - err: fmt.Errorf("config validation failed: request_timeout must be positive"), - }, - { - in: `{ "request_timeout": "-3s" }`, - cfg: nil, - err: fmt.Errorf("config validation failed: request_timeout must be positive"), - }, - { - in: `{ "sleep_interval": "-3s" }`, - cfg: nil, - err: fmt.Errorf("config validation failed: sleep_interval must be positive"), - }, - { - in: `{ "keeper_fail_interval": "-3s" }`, - cfg: nil, - err: fmt.Errorf("config validation failed: keeper_fail_interval must be positive"), - }, - { - in: `{ "max_standbys_per_sender": 0 }`, - cfg: nil, - err: fmt.Errorf("config validation failed: max_standbys_per_sender must be at least 1"), - }, - // All options defined - { - in: `{ "request_timeout": "10s", "sleep_interval": "10s", "keeper_fail_interval": "100s", "max_standbys_per_sender": 5, "synchronous_replication": true, "init_with_multiple_keepers": true, - "pg_parameters": { - "param01": "value01" - } - }`, - cfg: mergeDefaults(&NilConfig{ - RequestTimeout: &Duration{10 * time.Second}, - SleepInterval: &Duration{10 * time.Second}, - KeeperFailInterval: &Duration{100 * time.Second}, - MaxStandbysPerSender: UintP(5), - SynchronousReplication: BoolP(true), - InitWithMultipleKeepers: BoolP(true), - PGParameters: &map[string]string{ - "param01": "value01", - }, - }).ToConfig(), - err: nil, - }, - } - - for i, tt := range tests { - var nilCfg *NilConfig - err := json.Unmarshal([]byte(tt.in), &nilCfg) - if err != nil { - if tt.err == nil { - t.Errorf("#%d: unexpected error: %v", i, err) - } else if tt.err.Error() != err.Error() { - t.Errorf("#%d: got error: %v, wanted error: %v", i, err, tt.err) - } - } else { - nilCfg.MergeDefaults() - cfg := nilCfg.ToConfig() - if tt.err != nil { - t.Errorf("#%d: got no error, wanted error: %v", i, tt.err) - } - if !reflect.DeepEqual(cfg, tt.cfg) { - t.Errorf(spew.Sprintf("#%d: wrong config: got: %#v, want: %#v", i, cfg, tt.cfg)) - } - } - - } -} - -func mergeDefaults(c *NilConfig) *NilConfig { - c.MergeDefaults() - return c -} - -func TestNilConfigCopy(t *testing.T) { - // cfg and origCfg are declared in an identical way. It's not - // possible to take a shallow copy since cfg must absolutely - // not change as it's used for the reflect.DeepEqual comparison. - cfg := mergeDefaults(&NilConfig{ - RequestTimeout: &Duration{10 * time.Second}, - SleepInterval: &Duration{10 * time.Second}, - KeeperFailInterval: &Duration{10 * time.Second}, - MaxStandbysPerSender: UintP(5), - SynchronousReplication: BoolP(true), - InitWithMultipleKeepers: BoolP(true), - PGParameters: &map[string]string{ - "param01": "value01", - }, - }) - origCfg := mergeDefaults(&NilConfig{ - RequestTimeout: &Duration{10 * time.Second}, - SleepInterval: &Duration{10 * time.Second}, - KeeperFailInterval: &Duration{10 * time.Second}, - MaxStandbysPerSender: UintP(5), - SynchronousReplication: BoolP(true), - InitWithMultipleKeepers: BoolP(true), - PGParameters: &map[string]string{ - "param01": "value01", - }, - }) - - // Now take a origCfg copy, change all its fields and check that origCfg isn't changed - newCfg := origCfg.Copy() - newCfg.RequestTimeout = &Duration{20 * time.Second} - newCfg.SleepInterval = &Duration{20 * time.Second} - newCfg.KeeperFailInterval = &Duration{20 * time.Second} - newCfg.MaxStandbysPerSender = UintP(10) - newCfg.SynchronousReplication = BoolP(false) - newCfg.InitWithMultipleKeepers = BoolP(false) - (*newCfg.PGParameters)["param01"] = "anothervalue01" - - if !reflect.DeepEqual(origCfg, cfg) { - t.Errorf("Original config shouldn't be changed") - } - -} - -func TestConfigCopy(t *testing.T) { - // cfg and origCfg are declared in an identical way. It's not - // possible to take a shallow copy since cfg must absolutely - // not change as it's used for the reflect.DeepEqual comparison. - cfg := mergeDefaults(&NilConfig{ - RequestTimeout: &Duration{10 * time.Second}, - SleepInterval: &Duration{10 * time.Second}, - KeeperFailInterval: &Duration{100 * time.Second}, - MaxStandbysPerSender: UintP(5), - SynchronousReplication: BoolP(true), - InitWithMultipleKeepers: BoolP(true), - PGParameters: &map[string]string{ - "param01": "value01", - }, - }).ToConfig() - origCfg := mergeDefaults(&NilConfig{ - RequestTimeout: &Duration{10 * time.Second}, - SleepInterval: &Duration{10 * time.Second}, - KeeperFailInterval: &Duration{100 * time.Second}, - MaxStandbysPerSender: UintP(5), - SynchronousReplication: BoolP(true), - InitWithMultipleKeepers: BoolP(true), - PGParameters: &map[string]string{ - "param01": "value01", - }, - }).ToConfig() - - // Now take a origCfg copy, change all its fields and check that origCfg isn't changed - newCfg := origCfg.Copy() - newCfg.RequestTimeout = 20 * time.Second - newCfg.SleepInterval = 20 * time.Second - newCfg.KeeperFailInterval = 20 * time.Second - newCfg.MaxStandbysPerSender = 10 - newCfg.SynchronousReplication = false - newCfg.InitWithMultipleKeepers = false - newCfg.PGParameters["param01"] = "anothervalue01" - - if !reflect.DeepEqual(origCfg, cfg) { - t.Errorf("Original config shouldn't be changed") - } - -} diff --git a/internal/common/common.go b/internal/common/common.go index 820ded393..c3eb14c2e 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,12 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package common contains some common constants and structures for stolon package common import ( "fmt" "io" - "io/ioutil" + "log" "os" "path" "reflect" @@ -27,33 +29,42 @@ import ( ) const ( + // StorePrefix is a default for the store-prefix parameter StorePrefix = "stolon/cluster" + // SentinelLeaderKey defines the key in the KeyValue store that is used for leader election of the sentinel SentinelLeaderKey = "sentinel-leader" ) +// PgUnixSocketDirectories is the default unix socket dorectories as passed when starting PostgreSQL const PgUnixSocketDirectories = "/tmp" +// Role is an enum defining the role of a PostgreSQL instance type Role string const ( + // RoleUndefined is set when the role cannot be deducted (e.a. no initialized data directory) RoleUndefined Role = "undefined" - RoleMaster Role = "master" - RoleStandby Role = "standby" + // RolePrimary is set when the data directory belongs to a primary (e.a. no recovery.conf) + RolePrimary Role = "master" + // RoleReplica is set when the data directory belongs to a replica (e.a. recovery.conf) + RoleReplica Role = "standby" ) // Roles enumerates all possible Role values var Roles = []Role{ RoleUndefined, - RoleMaster, - RoleStandby, + RolePrimary, + RoleReplica, } +// UID returns a new UID (4 bytes of a UUID) func UID() string { u := uuid.Must(uuid.NewV4()) return fmt.Sprintf("%x", u[:4]) } +// UUID returns a new UUID func UUID() string { return uuid.Must(uuid.NewV4()).String() } @@ -62,20 +73,25 @@ const ( stolonPrefix = "stolon_" ) +// StolonName returns the prefixed name func StolonName(name string) string { return stolonPrefix + name } +// NameFromStolonName returns the name without the prefix func NameFromStolonName(stolonName string) string { return strings.TrimPrefix(stolonName, stolonPrefix) } +// IsStolonName returns true if the passed value is prefixed func IsStolonName(name string) bool { return strings.HasPrefix(name, stolonPrefix) } +// Parameters is a map with PostgreSQL parameters type Parameters map[string]string +// Equals verifies 2 parameter objects to be the same func (s Parameters) Equals(is Parameters) bool { return reflect.DeepEqual(s, is) } @@ -101,11 +117,13 @@ func (s Parameters) Diff(newParams Parameters) []string { // temporary file and then moving it. writeFunc is the func that will write // data to the file. // This function is taken from -// https://github.com/youtube/vitess/blob/master/go/ioutil2/ioutil.go +// +// https://github.com/youtube/vitess/blob/master/go/ioutil2/ioutil.go +// // Copyright 2012, Google Inc. BSD-license, see licenses/LICENSE-BSD-3-Clause func WriteFileAtomicFunc(filename string, perm os.FileMode, writeFunc func(f io.Writer) error) error { dir, name := path.Split(filename) - f, err := ioutil.TempFile(dir, name) + f, err := os.CreateTemp(dir, name) if err != nil { return err } @@ -124,7 +142,9 @@ func WriteFileAtomicFunc(filename string, perm os.FileMode, writeFunc func(f io. } // Any err should result in full cleanup. if err != nil { - os.Remove(f.Name()) + if err := os.Remove(f.Name()); err != nil { + log.Fatalf("failed to remove temp file %s: %v", f.Name(), err) + } } return err } diff --git a/internal/common/common_suite_test.go b/internal/common/common_suite_test.go new file mode 100644 index 000000000..65d6be035 --- /dev/null +++ b/internal/common/common_suite_test.go @@ -0,0 +1,28 @@ +// Copyright 2026 PgVillage +// Copyright 2018 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package common_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTrace(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Common Suite") +} diff --git a/internal/common/common_test.go b/internal/common/common_test.go index 5855a1f62..69d390125 100644 --- a/internal/common/common_test.go +++ b/internal/common/common_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,30 +16,153 @@ package common_test import ( - "testing" + "io" + "os" + "slices" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/util" ) -func TestDiffReturnsChangedParams(t *testing.T) { - var curParams common.Parameters = map[string]string{ - "max_connections": "100", - "shared_buffers": "10MB", - "huge": "off", - } +var _ = Describe("UID", func() { + It("should return a new unique uid", func() { + uids := []string{common.UID()} + for i := 0; i < 100; i++ { + uid := common.UID() + Expect(slices.Contains(uids, uid)).To(BeFalse(), "Expected every new uid to be unique, but %s is duplicate after %d rounds", uid, len(uids)) + uids = append(uids, uid) + } + }) +}) - var newParams common.Parameters = map[string]string{ - "max_connections": "200", - "shared_buffers": "10MB", - "work_mem": "4MB", - } +var _ = Describe("UUID", func() { + It("should return a new unique uuid", func() { + uuids := []string{common.UUID()} + for i := 0; i < 100; i++ { + uuid := common.UUID() + Expect(slices.Contains(uuids, uuid)).To(BeFalse(), "Expected every new uuid to be unique, but %s is duplicate after %d rounds", uuid, len(uuids)) + uuids = append(uuids, uuid) + } + }) +}) - expectedDiff := []string{"max_connections", "huge", "work_mem"} +var _ = Describe("Parameters", func() { + Describe("Diff", func() { + It("should return the changed parameters", func() { + var curParams common.Parameters = map[string]string{ + "max_connections": "100", + "shared_buffers": "10MB", + "huge": "off", + } - diff := curParams.Diff(newParams) + var newParams common.Parameters = map[string]string{ + "max_connections": "200", + "shared_buffers": "10MB", + "work_mem": "4MB", + } - if !util.CompareStringSliceNoOrder(expectedDiff, diff) { - t.Errorf("Expected diff is %v, but got %v", expectedDiff, diff) - } -} + expectedDiff := []string{"max_connections", "huge", "work_mem"} + + diff := curParams.Diff(newParams) + Expect(util.CompareStringSliceNoOrder(expectedDiff, diff)).To(BeTrue(), "Expected diff is %v, but got %v", expectedDiff, diff) + }) + }) +}) + +var _ = Describe("StolonName", func() { + It("should return the stolon prefixed name", func() { + Expect(common.StolonName("test")).To(Equal("stolon_test")) + }) +}) + +var _ = Describe("NameFromStolonName", func() { + It("should return the name without the stolon prefix", func() { + Expect(common.NameFromStolonName("stolon_test")).To(Equal("test")) + }) +}) + +var _ = Describe("IsStolonName", func() { + It("should return true if the name is a stolon name", func() { + Expect(common.IsStolonName("stolon_test")).To(BeTrue()) + }) + + It("should return false if the name is not a stolon name", func() { + Expect(common.IsStolonName("test")).To(BeFalse()) + }) +}) + +var _ = Describe("Parameters.Equals", func() { + It("should return true for equal parameters", func() { + p1 := common.Parameters{"a": "1", "b": "2"} + p2 := common.Parameters{"a": "1", "b": "2"} + Expect(p1.Equals(p2)).To(BeTrue()) + }) + + It("should return false for non-equal parameters", func() { + p1 := common.Parameters{"a": "1", "b": "2"} + p2 := common.Parameters{"a": "1", "b": "3"} + Expect(p1.Equals(p2)).To(BeFalse()) + }) +}) + +var _ = Describe("WriteFileAtomic", func() { + var tmpDir string + BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "stolon-test") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(tmpDir) + }) + + It("should write the data to a file", func() { + filePath := tmpDir + "/test.txt" + data := []byte("test data") + err := common.WriteFileAtomic(filePath, 0644, data) + Expect(err).ToNot(HaveOccurred()) + + readData, err := os.ReadFile(filePath) + Expect(err).ToNot(HaveOccurred()) + Expect(readData).To(Equal(data)) + }) +}) + +var _ = Describe("WriteFileAtomicFunc", func() { + var tmpDir string + BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "stolon-test") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(tmpDir) + }) + + It("should write the data to a file using the provided function", func() { + filePath := tmpDir + "/test.txt" + data := []byte("test data") + err := common.WriteFileAtomicFunc(filePath, 0644, func(f io.Writer) error { + _, err := f.Write(data) + return err + }) + Expect(err).ToNot(HaveOccurred()) + + readData, err := os.ReadFile(filePath) + Expect(err).ToNot(HaveOccurred()) + Expect(readData).To(Equal(data)) + }) + + It("should return an error if the write function returns an error", func() { + filePath := tmpDir + "/test.txt" + err := common.WriteFileAtomicFunc(filePath, 0644, func(f io.Writer) error { + return io.ErrShortWrite + }) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(io.ErrShortWrite)) + }) +}) diff --git a/internal/common/tls.go b/internal/common/tls.go index 9aad31e85..50696f0eb 100644 --- a/internal/common/tls.go +++ b/internal/common/tls.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,20 +19,22 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" - "io/ioutil" + "fmt" + "os" ) +// NewTLSConfig returns a freshly initialized TLSConfig resource func NewTLSConfig(certFile, keyFile, caFile string, insecureSkipVerify bool) (*tls.Config, error) { tlsConfig := tls.Config{} // Populate root CA certs if caFile != "" { - pemBytes, err := ioutil.ReadFile(caFile) + pemBytes, err := os.ReadFile(caFile) if err != nil { return nil, err } roots := x509.NewCertPool() - + certsLoaded := 0 for { var block *pem.Block block, pemBytes = pem.Decode(pemBytes) @@ -43,11 +46,14 @@ func NewTLSConfig(certFile, keyFile, caFile string, insecureSkipVerify bool) (*t return nil, err } roots.AddCert(cert) + certsLoaded++ + } + if certsLoaded == 0 { + return nil, fmt.Errorf("no valid certificates found in CA file %q", caFile) } tlsConfig.RootCAs = roots } - // Populate keypair // both must be defined if certFile != "" && keyFile != "" { diff --git a/internal/common/tls_test.go b/internal/common/tls_test.go new file mode 100644 index 000000000..aadecbde9 --- /dev/null +++ b/internal/common/tls_test.go @@ -0,0 +1,136 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package common_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pgvillage-tools/stolon/internal/common" +) + +var _ = Describe("TLS", func() { + var ( + validCertsDir string + invalidCertsDir string + caCertPath, serverCertPath, serverKeyPath string + ) + + BeforeEach(func() { + var err error + validCertsDir, err = filepath.Abs("../../tests/testcerts/") + Expect(err).ToNot(HaveOccurred()) + + invalidCertsDir = filepath.Join(GinkgoT().TempDir(), "tls_test_certs") + err = os.MkdirAll(invalidCertsDir, 0755) + Expect(err).NotTo(HaveOccurred()) + + caCertPath = filepath.Join(validCertsDir, "ca.crt") + serverCertPath = filepath.Join(validCertsDir, "server.crt") + serverKeyPath = filepath.Join(validCertsDir, "server.key") + }) + + AfterEach(func() { + }) + + Context("NewTLSConfig", func() { + It("should return a tls.Config with CA certificate when only caFile is provided", func() { + config, err := common.NewTLSConfig("", "", caCertPath, false) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + Expect(config.RootCAs).NotTo(BeNil()) + Expect(config.Certificates).To(BeEmpty()) + Expect(config.InsecureSkipVerify).To(BeFalse()) + }) + + It("should return a tls.Config with client certificate when certFile and keyFile are provided", func() { + config, err := common.NewTLSConfig(serverCertPath, serverKeyPath, "", false) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + Expect(config.RootCAs).To(BeNil()) + Expect(config.Certificates).ToNot(BeEmpty()) + Expect(config.InsecureSkipVerify).To(BeFalse()) + }) + + It("should return a tls.Config with CA and client certificates when all files are provided", func() { + config, err := common.NewTLSConfig(serverCertPath, serverKeyPath, caCertPath, false) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + Expect(config.RootCAs).NotTo(BeNil()) + Expect(config.Certificates).ToNot(BeEmpty()) + Expect(config.InsecureSkipVerify).To(BeFalse()) + }) + + It("should set InsecureSkipVerify to true when specified", func() { + config, err := common.NewTLSConfig("", "", "", true) + Expect(err).ToNot(HaveOccurred()) + Expect(config).NotTo(BeNil()) + Expect(config.InsecureSkipVerify).To(BeTrue()) + }) + + It("should return an error when caFile does not exist", func() { + config, err := common.NewTLSConfig("", "", "/non/existent/ca.crt", false) + Expect(err).To(HaveOccurred()) + Expect(config).To(BeNil()) + }) + + It("should return an error when certFile does not exist", func() { + config, err := common.NewTLSConfig("/non/existent/server.crt", serverKeyPath, "", false) + Expect(err).To(HaveOccurred()) + Expect(config).To(BeNil()) + }) + + It("should return an error when keyFile does not exist", func() { + config, err := common.NewTLSConfig(serverCertPath, "/non/existent/server.key", "", false) + Expect(err).To(HaveOccurred()) + Expect(config).To(BeNil()) + }) + + It("should return an error with invalid caFile content", func() { + invalidCaPath := filepath.Join(invalidCertsDir, "invalid-ca.crt") + err := os.WriteFile(invalidCaPath, []byte("invalid cert content"), 0644) + Expect(err).ToNot(HaveOccurred()) + + config, err := common.NewTLSConfig("", "", invalidCaPath, false) + Expect(err).To(HaveOccurred()) + Expect(config).To(BeNil()) + }) + + It("should return an error with invalid certFile/keyFile content", func() { + invalidCertPath := filepath.Join(invalidCertsDir, "invalid-server.crt") + invalidKeyPath := filepath.Join(invalidCertsDir, "invalid-server.key") + err := os.WriteFile(invalidCertPath, []byte("invalid cert content"), 0644) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(invalidKeyPath, []byte("invalid key content"), 0644) + Expect(err).ToNot(HaveOccurred()) + + config, err := common.NewTLSConfig(invalidCertPath, invalidKeyPath, "", false) + Expect(err).To(HaveOccurred()) + Expect(config).To(BeNil()) + }) + }) +}) + +// copyFile is a helper function to copy a file from src to dst. +func copyFile(dst, src string) { + input, err := os.ReadFile(src) + Expect(err).ToNot(HaveOccurred()) + + err = os.WriteFile(dst, input, 0644) + Expect(err).ToNot(HaveOccurred()) +} diff --git a/internal/flagutil/env.go b/internal/flagutil/env.go index bac271f75..966302978 100644 --- a/internal/flagutil/env.go +++ b/internal/flagutil/env.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package flagutil implements all commandline flags package flagutil import ( @@ -29,7 +31,7 @@ import ( // variables additionally are prefixed by the given string followed by // and underscore. For example, if prefix=PREFIX: some-flag => PREFIX_SOME_FLAG func SetFlagsFromEnv(fs *flag.FlagSet, prefix string) (err error) { - alreadySet := make(map[string]bool) + alreadySet := map[string]bool{} fs.Visit(func(f *flag.Flag) { alreadySet[f.Name] = true }) diff --git a/internal/log/log.go b/internal/log/log.go deleted file mode 100644 index ff729a7ea..000000000 --- a/internal/log/log.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2017 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package log - -import ( - "fmt" - "log" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -var ( - s *zap.SugaredLogger - sColor *zap.SugaredLogger -) - -// default info level -var level = zap.NewAtomicLevelAt(zapcore.InfoLevel) - -func init() { - config := zap.Config{ - Level: level, - Development: false, - DisableStacktrace: true, - Encoding: "console", - EncoderConfig: zap.NewDevelopmentEncoderConfig(), - OutputPaths: []string{"stderr"}, - ErrorOutputPaths: []string{"stderr"}, - } - - logger, err := config.Build() - if err != nil { - panic(fmt.Errorf("failed to initialize logger: %v", err)) - } - s = logger.Sugar() - - config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder - - logger, err = config.Build() - if err != nil { - panic(fmt.Errorf("failed to initialize color logger: %v", err)) - } - sColor = logger.Sugar() -} - -func SetDebug() { - level.SetLevel(zapcore.DebugLevel) -} - -func SetLevel(lvl zapcore.Level) { - level.SetLevel(lvl) -} - -func IsDebug() bool { - return level.Level() == zapcore.DebugLevel -} - -func S() *zap.SugaredLogger { - return s -} - -func StdLog() *log.Logger { - return zap.NewStdLog(s.Desugar()) -} - -func SColor() *zap.SugaredLogger { - return sColor -} - -func StdLogColor() *log.Logger { - return zap.NewStdLog(sColor.Desugar()) -} diff --git a/internal/logging/components.go b/internal/logging/components.go new file mode 100644 index 000000000..28dbeb864 --- /dev/null +++ b/internal/logging/components.go @@ -0,0 +1,113 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package logging brings a generic logging interface +package logging + +import ( + "strings" + + "github.com/rs/zerolog" +) + +// Components is a map that holds components and their Debug state (false is InfoLevel, True is DebugLevel) +type Components map[Component]zerolog.Level + +// Component is a custom type, so that we can use it as an ENUM +type Component int + +const ( + // KeeperComponent is the main component specifically for the Keeper commands + KeeperComponent Component = iota + // ProxyComponent is the main component specifically for the Proxy commands + ProxyComponent Component = iota + // SentinelComponent is the main component specifically for the Sentinel commands + SentinelComponent Component = iota + // CmdComponent is the main component specifically for stolon-cmd commands + CmdComponent Component = iota + + // PgComponent is the component for all PostgreSQL logging + PgComponent Component = iota + // PgUtilsComponent is the component for all PostgreSQL logging + PgUtilsComponent Component = iota + + // StoreComponent is the logging component for all code dealing with KV stores + StoreComponent Component = iota + + // UnknownComponent represents a logging component with unknown origin + UnknownComponent Component = iota + // TestComponent represents a logging component only used in unittests + TestComponent Component = iota +) + +var ( + componentConverter = map[string]Component{ + "keeper": KeeperComponent, + "proxy": ProxyComponent, + "sentinel": SentinelComponent, + "stolon-cmd": CmdComponent, + "postgres": PgComponent, + "postgres-utils": PgUtilsComponent, + "kv-store": StoreComponent, + "undefined_component": UnknownComponent, + "unittest_component": TestComponent, + } + reverseComponentMap map[Component]string +) + +func componentToString(component Component) string { + if reverseComponentMap == nil { + reverseComponentMap = map[Component]string{} + for s, comp := range componentConverter { + reverseComponentMap[comp] = s + } + } + if s, exists := reverseComponentMap[component]; exists { + return s + } + return "undefined_component" +} + +// DebugComponentsFromString takes a comma separated string (as used in a command argument) +// and returns a Components object with all items set to debug +func DebugComponentsFromString(commaSeparated string) Components { + components := Components{} + for _, compName := range strings.Split(commaSeparated, ",") { + if component, exists := componentConverter[compName]; exists { + components[component] = zerolog.DebugLevel + } else { + components[UnknownComponent] = zerolog.DebugLevel + } + } + return components +} + +// NewComponentsFromStringMap takes a string map and converts it into a Components object +// where every component is set to the level represented by the string +func NewComponentsFromStringMap(enabledComponents map[string]string) Components { + components := Components{} + for compName, sLevel := range enabledComponents { + level, ok := sToLevel[sLevel] + if !ok { + level = zerolog.DebugLevel + } + if component, exists := componentConverter[compName]; exists { + components[component] = level + } else { + components[UnknownComponent] = level + } + } + return components +} diff --git a/internal/logging/components_test.go b/internal/logging/components_test.go new file mode 100644 index 000000000..3da92257e --- /dev/null +++ b/internal/logging/components_test.go @@ -0,0 +1,72 @@ +package logging + +import ( + "strings" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestComponentToString(t *testing.T) { + var nonexistentComponent Component = -1 + assert.Equal(t, componentToString(nonexistentComponent), componentToString(UnknownComponent)) +} + +func TestNewComponentsFromString(t *testing.T) { + var ( + components = []string{ + "keeper", + "proxy", + "undefined_component", + "unittest_component", + } + commaSeparatedString = strings.Join(components, ",") + result = DebugComponentsFromString(commaSeparatedString) + ) + for _, compName := range components { + comp, exists := componentConverter[compName] + if !exists { + comp = UnknownComponent + } + require.Contains(t, result, comp) + assert.Equal(t, zerolog.DebugLevel, result[comp]) + } +} + +func TestNewComponentsFromStringMap(t *testing.T) { + var ( + debugComponents = []string{ + "keeper", + "proxy", + } + warnComponents = []string{ + "sentinel", + "stolon-cmd", + } + strMap = map[string]string{} + ) + for _, compName := range debugComponents { + strMap[compName] = "debug" + } + for _, compName := range warnComponents { + strMap[compName] = "warn" + } + result := NewComponentsFromStringMap(strMap) + + for _, compName := range debugComponents { + comp, exists := componentConverter[compName] + require.True(t, exists) + level, exists := result[comp] + require.True(t, exists) + require.Equal(t, level, zerolog.DebugLevel) + } + for _, compName := range warnComponents { + comp, exists := componentConverter[compName] + require.True(t, exists) + level, exists := result[comp] + require.True(t, exists) + require.Equal(t, level, zerolog.WarnLevel) + } +} diff --git a/internal/logging/main.go b/internal/logging/main.go new file mode 100644 index 000000000..69368ec00 --- /dev/null +++ b/internal/logging/main.go @@ -0,0 +1,134 @@ +/* +Copyright 2023, Tax Administration of The Netherlands. +Licensed under the EUPL 1.2. +See LICENSE.md for details. +*/ + +package logging + +import ( + "context" + "os" + "time" + + "github.com/google/uuid" + "github.com/mattn/go-isatty" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + defaultLevel = zerolog.InfoLevel +) + +var ( + // Commandline args will use this to enable all debug logging + staticLevel = defaultLevel + // Commandline args can use this to enable logging for a component + staticComponents = Components{} + // Commandline args will use this to enable all debug logging + dynamicLevel = defaultLevel + // Commandline args can use this to enable logging for a component + dynamicComponents = Components{} + // The output logger to be used + output zerolog.ConsoleWriter + // sToLevel is a map to easilly convert between string and zerolog level + sToLevel = map[string]zerolog.Level{ + "debug": zerolog.DebugLevel, + "info": zerolog.InfoLevel, + "error": zerolog.ErrorLevel, + "warn": zerolog.WarnLevel, + "warning": zerolog.WarnLevel, + } +) + +func init() { + output = zerolog.ConsoleWriter{ + Out: os.Stdout, + NoColor: !doColor(), + TimeFormat: time.RFC3339, + } + l := zerolog.New(output).With().Timestamp().Logger() + + log.Logger = l + zerolog.DefaultContextLogger = &l +} + +func doColor() bool { + if os.Getenv("NO_COLOR") != "" { + return true + } + return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) +} + +// SetStaticLevel configures global debugging level from commandline argument +func SetStaticLevel(level string) { + staticLevel = defaultLevel + if lvl, ok := sToLevel[level]; ok { + staticLevel = lvl + } +} + +// SetStaticComponents configures component debugging from commandline argument +func SetStaticComponents(components Components) { + if components == nil { + components = Components{} + } + staticComponents = components +} + +// SetDynamicLoggingConfig configures global debugging and component debugging +func SetDynamicLoggingConfig(level string, components Components) { + dynamicLevel = defaultLevel + if lvl, ok := sToLevel[level]; ok { + dynamicLevel = lvl + } + if components == nil { + components = Components{} + } + dynamicComponents = components +} + +func getComponentLevel(name Component) zerolog.Level { + if level, exists := dynamicComponents[name]; exists { + return level + } + if level, ok := staticComponents[name]; ok { + return level + } + if staticLevel < dynamicLevel { + return staticLevel + } + return dynamicLevel +} + +// GetLogComponent gets the logger for a component from a context. +func GetLogComponent(ctx context.Context, comp Component) (context.Context, *zerolog.Logger) { + logger := log.Ctx(ctx) + level := getComponentLevel(comp) + + if logger.GetLevel() != level { + ll := logger. + Output(output). + Level(level). + With(). + Str("ID", uuid.NewString()). + Str("component", componentToString(comp)). + Logger() + logger = &ll + ctx = logger.WithContext(ctx) + } + return ctx, logger +} + +// EnableColor will force logging with colors +// (default depends on if we run in a terminal) +func EnableColor() { + output.NoColor = false +} + +// DisableColor will force logging with colors +// (default depends on if we run in a terminal) +func DisableColor() { + output.NoColor = true +} diff --git a/internal/logging/main_test.go b/internal/logging/main_test.go new file mode 100644 index 000000000..db8585c40 --- /dev/null +++ b/internal/logging/main_test.go @@ -0,0 +1,153 @@ +package logging + +import ( + "bytes" + "context" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +const ( + defaultLevelString = "info" + enabledLevelString = "debug" +) + +type logSink struct { + logs []string + buf bytes.Buffer +} + +func (l *logSink) Write(p []byte) (n int, err error) { + l.logs = append(l.logs, string(p)) + return l.buf.Write(p) +} + +func (l *logSink) Index(i int) string { + if i >= 0 && i < len(l.logs) { + return l.logs[i] + } + return "" +} + +func (l *logSink) String() string { + return l.buf.String() +} + +func (l *logSink) Reset() { + l.logs = []string{} + l.buf.Reset() +} + +func TestDebuggingStatic(t *testing.T) { + const comp1 = TestComponent + SetDynamicLoggingConfig(defaultLevelString, nil) + ctx := context.TODO() + // debug false + SetStaticLevel(defaultLevelString) + _, noDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.InfoLevel, noDebugLogger.GetLevel()) + // debug true + SetStaticLevel(enabledLevelString) + _, allDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.DebugLevel, allDebugLogger.GetLevel()) + // debug component + SetStaticComponents(Components{comp1: zerolog.DebugLevel}) + _, componentDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.DebugLevel, componentDebugLogger.GetLevel()) +} + +func TestDebuggingDynamic(t *testing.T) { + ctx := context.TODO() + const comp1 = TestComponent + SetStaticLevel(defaultLevelString) + SetStaticComponents(nil) + // debug false + SetDynamicLoggingConfig(defaultLevelString, nil) + _, noDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.InfoLevel, noDebugLogger.GetLevel()) + // debug true + SetDynamicLoggingConfig(enabledLevelString, nil) + _, allDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.DebugLevel, allDebugLogger.GetLevel()) + // debug component on + SetDynamicLoggingConfig(defaultLevelString, Components{comp1: zerolog.DebugLevel}) + _, componentDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.DebugLevel, componentDebugLogger.GetLevel()) + + // debug component off + SetDynamicLoggingConfig(defaultLevelString, Components{}) + _, componentNoDebugLogger := GetLogComponent(ctx, comp1) + assert.Equal(t, zerolog.InfoLevel, componentNoDebugLogger.GetLevel()) +} + +func TestOutput(t *testing.T) { + // Keep original output + originalOut := output.Out + sink := &logSink{} + output.Out = sink + // Restore original output after test + defer func() { + output.Out = originalOut + }() + + ctx := context.TODO() + const comp = TestComponent + + // Test Info level + SetStaticLevel("info") + SetStaticComponents(nil) + SetDynamicLoggingConfig("info", nil) + _, logger := GetLogComponent(ctx, comp) + + logger.Debug().Msg("this is a debug message") + assert.Empty(t, sink.String(), "Debug message should not be logged at info level") + sink.Reset() + + logger.Info().Msg("this is an info message") + assert.Contains(t, sink.String(), "this is an info message", "Info message should be logged at info level") + sink.Reset() + + logger.Warn().Msg("this is a warning message") + assert.Contains(t, sink.String(), "this is a warning message", "Warning message should be logged at info level") + sink.Reset() + + // Test Error level + SetStaticLevel("error") + SetDynamicLoggingConfig("error", nil) + _, logger = GetLogComponent(ctx, comp) + logger.Warn().Msg("this is another warning message") + assert.Empty(t, sink.String(), "Warning message should not be logged at error level") + sink.Reset() + + logger.Error().Msg("this is an error message") + assert.Contains(t, sink.String(), "this is an error message", "Error message should be logged at error level") + sink.Reset() +} + +func TestInvalidLevel(t *testing.T) { + // Test that setting an invalid level falls back to the default (info) + const comp = TestComponent + ctx := context.TODO() + SetStaticLevel("invalidlevel") + SetStaticComponents(nil) + SetDynamicLoggingConfig("anotherinvalidlevel", nil) + + _, logger := GetLogComponent(ctx, comp) + + assert.Equal(t, zerolog.InfoLevel, logger.GetLevel(), "Logger level should default to Info for invalid level strings") +} + +func TestColoring(t *testing.T) { + originalNoColor := output.NoColor + + EnableColor() + assert.False(t, output.NoColor, "EnableColor should set NoColor to false") + + DisableColor() + assert.True(t, output.NoColor, "DisableColor should set NoColor to true") + + // Restore original + output.NoColor = originalNoColor +} diff --git a/internal/mock/postgresql/postgresql.go b/internal/mock/postgresql/postgresql.go index 935caa841..bb9545a00 100644 --- a/internal/mock/postgresql/postgresql.go +++ b/internal/mock/postgresql/postgresql.go @@ -5,9 +5,11 @@ package mocks import ( - gomock "github.com/golang/mock/gomock" - postgresql "github.com/sorintlab/stolon/internal/postgresql" + "context" reflect "reflect" + + gomock "github.com/golang/mock/gomock" + postgresql "github.com/pgvillage-tools/stolon/internal/postgresql" ) // MockPGManager is a mock of PGManager interface @@ -34,7 +36,7 @@ func (m *MockPGManager) EXPECT() *MockPGManagerMockRecorder { } // GetTimelinesHistory mocks base method -func (m *MockPGManager) GetTimelinesHistory(timeline uint64) ([]*postgresql.TimelineHistory, error) { +func (m *MockPGManager) GetTimelinesHistory(_ context.Context, timeline uint64) ([]*postgresql.TimelineHistory, error) { ret := m.ctrl.Call(m, "GetTimelinesHistory", timeline) ret0, _ := ret[0].([]*postgresql.TimelineHistory) ret1, _ := ret[1].(error) @@ -42,6 +44,6 @@ func (m *MockPGManager) GetTimelinesHistory(timeline uint64) ([]*postgresql.Time } // GetTimelinesHistory indicates an expected call of GetTimelinesHistory -func (mr *MockPGManagerMockRecorder) GetTimelinesHistory(timeline interface{}) *gomock.Call { +func (mr *MockPGManagerMockRecorder) GetTimelinesHistory(timeline any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTimelinesHistory", reflect.TypeOf((*MockPGManager)(nil).GetTimelinesHistory), timeline) } diff --git a/internal/mock/store/store.go b/internal/mock/store/store.go index 0764112c8..ec6d588c4 100644 --- a/internal/mock/store/store.go +++ b/internal/mock/store/store.go @@ -6,11 +6,12 @@ package mock_store import ( context "context" - gomock "github.com/golang/mock/gomock" - cluster "github.com/sorintlab/stolon/internal/cluster" - store "github.com/sorintlab/stolon/internal/store" reflect "reflect" time "time" + + gomock "github.com/golang/mock/gomock" + cluster "github.com/pgvillage-tools/stolon/api/v1" + store "github.com/pgvillage-tools/stolon/internal/store" ) // MockStore is a mock of Store interface @@ -37,7 +38,7 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { } // AtomicPutClusterData mocks base method -func (m *MockStore) AtomicPutClusterData(ctx context.Context, cd *cluster.ClusterData, previous *store.KVPair) (*store.KVPair, error) { +func (m *MockStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Data, previous *store.KVPair) (*store.KVPair, error) { ret := m.ctrl.Call(m, "AtomicPutClusterData", ctx, cd, previous) ret0, _ := ret[0].(*store.KVPair) ret1, _ := ret[1].(error) @@ -45,33 +46,33 @@ func (m *MockStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Cluste } // AtomicPutClusterData indicates an expected call of AtomicPutClusterData -func (mr *MockStoreMockRecorder) AtomicPutClusterData(ctx, cd, previous interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AtomicPutClusterData(ctx, cd, previous any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AtomicPutClusterData", reflect.TypeOf((*MockStore)(nil).AtomicPutClusterData), ctx, cd, previous) } // PutClusterData mocks base method -func (m *MockStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) error { +func (m *MockStore) PutClusterData(ctx context.Context, cd *cluster.Data) error { ret := m.ctrl.Call(m, "PutClusterData", ctx, cd) ret0, _ := ret[0].(error) return ret0 } // PutClusterData indicates an expected call of PutClusterData -func (mr *MockStoreMockRecorder) PutClusterData(ctx, cd interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) PutClusterData(ctx, cd any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutClusterData", reflect.TypeOf((*MockStore)(nil).PutClusterData), ctx, cd) } // GetClusterData mocks base method -func (m *MockStore) GetClusterData(ctx context.Context) (*cluster.ClusterData, *store.KVPair, error) { +func (m *MockStore) GetClusterData(ctx context.Context) (*cluster.Data, *store.KVPair, error) { ret := m.ctrl.Call(m, "GetClusterData", ctx) - ret0, _ := ret[0].(*cluster.ClusterData) + ret0, _ := ret[0].(*cluster.Data) ret1, _ := ret[1].(*store.KVPair) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // GetClusterData indicates an expected call of GetClusterData -func (mr *MockStoreMockRecorder) GetClusterData(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetClusterData(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterData", reflect.TypeOf((*MockStore)(nil).GetClusterData), ctx) } @@ -83,7 +84,7 @@ func (m *MockStore) SetKeeperInfo(ctx context.Context, id string, ms *cluster.Ke } // SetKeeperInfo indicates an expected call of SetKeeperInfo -func (mr *MockStoreMockRecorder) SetKeeperInfo(ctx, id, ms, ttl interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SetKeeperInfo(ctx, id, ms, ttl any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetKeeperInfo", reflect.TypeOf((*MockStore)(nil).SetKeeperInfo), ctx, id, ms, ttl) } @@ -96,7 +97,7 @@ func (m *MockStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, er } // GetKeepersInfo indicates an expected call of GetKeepersInfo -func (mr *MockStoreMockRecorder) GetKeepersInfo(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetKeepersInfo(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeepersInfo", reflect.TypeOf((*MockStore)(nil).GetKeepersInfo), ctx) } @@ -108,7 +109,7 @@ func (m *MockStore) SetSentinelInfo(ctx context.Context, si *cluster.SentinelInf } // SetSentinelInfo indicates an expected call of SetSentinelInfo -func (mr *MockStoreMockRecorder) SetSentinelInfo(ctx, si, ttl interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SetSentinelInfo(ctx, si, ttl any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSentinelInfo", reflect.TypeOf((*MockStore)(nil).SetSentinelInfo), ctx, si, ttl) } @@ -121,7 +122,7 @@ func (m *MockStore) GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo } // GetSentinelsInfo indicates an expected call of GetSentinelsInfo -func (mr *MockStoreMockRecorder) GetSentinelsInfo(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetSentinelsInfo(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSentinelsInfo", reflect.TypeOf((*MockStore)(nil).GetSentinelsInfo), ctx) } @@ -133,7 +134,7 @@ func (m *MockStore) SetProxyInfo(ctx context.Context, pi *cluster.ProxyInfo, ttl } // SetProxyInfo indicates an expected call of SetProxyInfo -func (mr *MockStoreMockRecorder) SetProxyInfo(ctx, pi, ttl interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) SetProxyInfo(ctx, pi, ttl any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProxyInfo", reflect.TypeOf((*MockStore)(nil).SetProxyInfo), ctx, pi, ttl) } @@ -146,7 +147,7 @@ func (m *MockStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, er } // GetProxiesInfo indicates an expected call of GetProxiesInfo -func (mr *MockStoreMockRecorder) GetProxiesInfo(ctx interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProxiesInfo(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProxiesInfo", reflect.TypeOf((*MockStore)(nil).GetProxiesInfo), ctx) } diff --git a/internal/postgresql/conn_params.go b/internal/postgresql/conn_params.go new file mode 100644 index 000000000..685711d8f --- /dev/null +++ b/internal/postgresql/conn_params.go @@ -0,0 +1,132 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +import ( + "fmt" + "maps" + "reflect" + "sort" + "strings" +) + +// This is based on github.com/lib/pq + +// ConnParams defines key/value pairs for connecting to PostgreSQL +type ConnParams map[ConnParamKey]string + +// Set can add/update a key +func (cp ConnParams) Set(k ConnParamKey, v string) { + cp[k] = v +} + +// Get can retrieve a key/value pai +func (cp ConnParams) Get(k ConnParamKey) (v string) { + return cp[k] +} + +// Del can remove a key +func (cp ConnParams) Del(k ConnParamKey) { + delete(cp, k) +} + +// Isset returns true if the key is set +func (cp ConnParams) Isset(k ConnParamKey) bool { + _, ok := cp[k] + return ok +} + +// Equals checks 2 ConnParams to be the same +func (cp ConnParams) Equals(cp2 ConnParams) bool { + return reflect.DeepEqual(cp, cp2) +} + +// Copy returns a shallow copy +func (cp ConnParams) Copy() ConnParams { + ncp := ConnParams{} + for k, v := range cp { + ncp[k] = v + } + return ncp +} + +// ConnString returns a connection string, its entries are sorted so the +// returned string can be reproducible and comparable +func (cp ConnParams) ConnString() string { + var kvs []string + escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) + for k, v := range cp { + if v != "" { + kvs = append(kvs, fmt.Sprintf("%s=%s", k, escaper.Replace(v))) + } + } + sort.Strings(kvs) + return strings.Join(kvs, " ") +} + +// WithUser returns a clone with the user fields set to the specified userName +func (cp ConnParams) WithUser(userName string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyUser] = userName + return q +} + +// WithHost returns a clone with the host fields set to the specified hostName +func (cp ConnParams) WithHost(hostName string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyHost] = hostName + return q +} + +// WithDbName returns a clone with the host fields set to the specified hostName +func (cp ConnParams) WithDbName(dbName string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyDbName] = dbName + return q +} + +// WithPort returns a clone with the port fields set as specified +func (cp ConnParams) WithPort(port uint) ConnParams { + return cp.WithSPort(fmt.Sprintf("%d", port)) +} + +// WithSPort returns a clone with the port fields set as specified +func (cp ConnParams) WithSPort(port string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyPort] = port + return q +} + +// WithAppName returns a clone with the port fields set as specified +func (cp ConnParams) WithAppName(appName string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyAppName] = appName + return q +} + +// WithSSLMode returns a clone with the port fields set as specified +func (cp ConnParams) WithSSLMode(sslMode string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeySSLMode] = sslMode + return q +} + +// WithPassword returns a clone with the password field set as specified +func (cp ConnParams) WithPassword(password string) ConnParams { + q := maps.Clone(cp) + q[ConnParamKeyPassword] = password + return q +} diff --git a/internal/postgresql/conn_params_test.go b/internal/postgresql/conn_params_test.go new file mode 100644 index 000000000..7052bda57 --- /dev/null +++ b/internal/postgresql/conn_params_test.go @@ -0,0 +1,86 @@ +package postgresql + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ConnParams", func() { + When("Using Set Get and Del", func() { + It("should work as expected", func() { + const ( + key = "mykey1" + value = "myvalue1" + ) + cp := ConnParams{} + Ω(cp.Get(key)).To(BeEmpty()) + Ω(cp.Isset(key)).To(BeFalse()) + cp.Set(key, value) + Ω(cp.Get(key)).To(Equal(value)) + Ω(cp.Isset(key)).To(BeTrue()) + cp.Del(key) + Ω(cp.Get(key)).To(BeEmpty()) + Ω(cp.Isset(key)).To(BeFalse()) + }) + }) + When("Comparing", func() { + It("should work as expected", func() { + cp1 := ConnParams{ + ConnParamKeyHost: "::1", + ConnParamKeyPort: "5432", + } + cp2 := cp1.Copy() + Ω(cp1.Equals(cp2)).To(BeTrue()) + cp2[ConnParamKeyHost] = "127.0.0.1" + Ω(cp1.Equals(cp2)).To(BeFalse()) + cp2[ConnParamKeyHost] = cp1[ConnParamKeyHost] + Ω(cp1.Equals(cp2)).To(BeTrue()) + }) + }) + When("Parsing", func() { + It("should work as expected", func() { + cn, err := ParseConnString(`host=myhost port=5433 f=v`) + Ω(err).NotTo(HaveOccurred()) + Ω(cn).To(HaveKeyWithValue(ConnParamKeyHost, "myhost")) + Ω(cn).To(HaveKeyWithValue(ConnParamKey("f"), "v")) + }) + }) + When("Parsing url", func() { + It("should work as expected", func() { + for _, url := range []string{ + `postgres://myself:password@myhost:5434/mydb1`, + `postgresql://myself:password@myhost:5434/mydb1`, + } { + cn, err := URLToConnParams(url) + Ω(err).NotTo(HaveOccurred()) + Ω(cn).To(HaveKeyWithValue(ConnParamKeyHost, "myhost")) + Ω(cn).To(HaveKeyWithValue(ConnParamKeyPort, "5434")) + Ω(cn).To(HaveKeyWithValue(ConnParamKeyUser, "myself")) + } + }) + }) + When("Getting as connstring", func() { + It("should work as expected", func() { + cn, err := URLToConnParams(`postgresql://myself:password@myhost:5434/mydb1`) + Ω(err).NotTo(HaveOccurred()) + Ω(cn.ConnString()).To(Equal("dbname=mydb1 host=myhost password=password port=5434 user=myself")) + }) + }) + When("Getting a clone with other settings", func() { + It("should work as expected", func() { + cp1 := ConnParams{ + ConnParamKeyHost: "::1", + ConnParamKeyPort: "5432", + ConnParamKeyUser: "myself", + } + cp2 := cp1.WithUser("someoneelse"). + WithAppName("myapp"). + WithPort(5433). + WithSSLMode("verify-full") + Ω(cp1).To(HaveKeyWithValue(ConnParamKeyUser, "myself")) + Ω(cp2).To(HaveKeyWithValue(ConnParamKeyUser, "someoneelse")) + Ω(cp2).To(HaveKeyWithValue(ConnParamKeyHost, cp1[ConnParamKeyHost])) + Ω(cp2).To(HaveKeyWithValue(ConnParamKeyPort, "5433")) + }) + }) +}) diff --git a/internal/postgresql/connstring.go b/internal/postgresql/connstring.go deleted file mode 100644 index de29ab571..000000000 --- a/internal/postgresql/connstring.go +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2015 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package postgresql - -import ( - "fmt" - "net/url" - "reflect" - "sort" - "strings" - "unicode" -) - -// This is based on github.com/lib/pq - -type ConnParams map[string]string - -func (cp ConnParams) Set(k, v string) { - cp[k] = v -} - -func (cp ConnParams) Get(k string) (v string) { - return cp[k] -} - -func (cp ConnParams) Del(k string) { - delete(cp, k) -} - -func (cp ConnParams) Isset(k string) bool { - _, ok := cp[k] - return ok -} - -func (cp ConnParams) Equals(cp2 ConnParams) bool { - return reflect.DeepEqual(cp, cp2) -} - -func (cp ConnParams) Copy() ConnParams { - ncp := ConnParams{} - for k, v := range cp { - ncp[k] = v - } - return ncp -} - -// scanner implements a tokenizer for libpq-style option strings. -type scanner struct { - s []rune - i int -} - -// newScanner returns a new scanner initialized with the option string s. -func newScanner(s string) *scanner { - return &scanner{[]rune(s), 0} -} - -// Next returns the next rune. -// It returns 0, false if the end of the text has been reached. -func (s *scanner) Next() (rune, bool) { - if s.i >= len(s.s) { - return 0, false - } - r := s.s[s.i] - s.i++ - return r, true -} - -// SkipSpaces returns the next non-whitespace rune. -// It returns 0, false if the end of the text has been reached. -func (s *scanner) SkipSpaces() (rune, bool) { - r, ok := s.Next() - for unicode.IsSpace(r) && ok { - r, ok = s.Next() - } - return r, ok -} - -// ParseConnString parses the options from name and adds them to the values. -// -// The parsing code is based on conninfo_parse from libpq's fe-connect.c -func ParseConnString(name string) (ConnParams, error) { - p := make(ConnParams) - s := newScanner(name) - - for { - var ( - keyRunes, valRunes []rune - r rune - ok bool - ) - - if r, ok = s.SkipSpaces(); !ok { - break - } - - // Scan the key - for !unicode.IsSpace(r) && r != '=' { - keyRunes = append(keyRunes, r) - if r, ok = s.Next(); !ok { - break - } - } - - // Skip any whitespace if we're not at the = yet - if r != '=' { - r, ok = s.SkipSpaces() - } - - // The current character should be = - if r != '=' || !ok { - return nil, fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) - } - - // Skip any whitespace after the = - if r, ok = s.SkipSpaces(); !ok { - // If we reach the end here, the last value is just an empty string as per libpq. - p.Set(string(keyRunes), "") - break - } - - if r != '\'' { - for !unicode.IsSpace(r) { - if r == '\\' { - if r, ok = s.Next(); !ok { - return nil, fmt.Errorf(`missing character after backslash`) - } - } - valRunes = append(valRunes, r) - - if r, ok = s.Next(); !ok { - break - } - } - } else { - quote: - for { - if r, ok = s.Next(); !ok { - return nil, fmt.Errorf(`unterminated quoted string literal in connection string`) - } - switch r { - case '\'': - break quote - case '\\': - r, _ = s.Next() - fallthrough - default: - valRunes = append(valRunes, r) - } - } - } - - p.Set(string(keyRunes), string(valRunes)) - } - - return p, nil -} - -// URLToConnParams creates the connParams from the url. -func URLToConnParams(urlStr string) (ConnParams, error) { - p := make(ConnParams) - u, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - - if u.Scheme != "postgres" { - return nil, fmt.Errorf("invalid connection protocol: %s", u.Scheme) - } - - if u.User != nil { - v := u.User.Username() - p.Set("user", v) - v, _ = u.User.Password() - p.Set("password", v) - } - - i := strings.Index(u.Host, ":") - if i < 0 { - p.Set("host", u.Host) - } else { - p.Set("host", u.Host[:i]) - p.Set("port", u.Host[i+1:]) - } - - if u.Path != "" { - p.Set("dbname", u.Path[1:]) - } - - q := u.Query() - for k := range q { - p.Set(k, q.Get(k)) - } - - return p, nil -} - -// ConnString returns a connection string, its entries are sorted so the -// returned string can be reproducible and comparable -func (p ConnParams) ConnString() string { - var kvs []string - escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) - for k, v := range p { - if v != "" { - kvs = append(kvs, k+"="+escaper.Replace(v)) - } - } - sort.Strings(kvs) - return strings.Join(kvs, " ") -} diff --git a/internal/postgresql/init_config.go b/internal/postgresql/init_config.go new file mode 100644 index 000000000..4666d9319 --- /dev/null +++ b/internal/postgresql/init_config.go @@ -0,0 +1,23 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +// InitConfig stores the config specific to initdb +type InitConfig struct { + Locale string + Encoding string + DataChecksums bool +} diff --git a/internal/postgresql/main.go b/internal/postgresql/main.go new file mode 100644 index 000000000..362cbcbea --- /dev/null +++ b/internal/postgresql/main.go @@ -0,0 +1,962 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package postgresql has all stolon specific postgresql code +package postgresql + +import ( + "bufio" + "context" + + // TODO: replace with jackc + "database/sql" + "errors" + "fmt" + "io" + "io/fs" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + "time" + "unicode" + + "github.com/Masterminds/semver/v3" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/logging" + "github.com/rs/zerolog" + + // TODO: This can probably go + "github.com/lib/pq" +) + +const ( + postgresConf = "postgresql.conf" + postgresRecoveryConf = "recovery.conf" + postgresStandbySignal = "standby.signal" + postgresRecoverySignal = "recovery.signal" + postgresRecoveryDone = "recovery.done" + postgresAutoConf = "postgresql.auto.conf" + tmpPostgresConf = "stolon-temp-postgresql.conf" + + startTimeout = 60 * time.Second + exitStatusNotRunning = 3 + exitStatusInaccessibleDatadir = 4 + + argDatadir = "-D" + argDbName = "-d" + logExec = "execing cmd" +) + +var ( + // ErrInaccessibleDatadir is raised when pg_ctl returns an unknown state + ErrInaccessibleDatadir = errors.New("unknown postgres state") + + utlCtx context.Context + utlLogger *zerolog.Logger +) + +func init() { + utlCtx, utlLogger = logging.GetLogComponent(context.Background(), logging.PgUtilsComponent) +} + +// TODO: implement all other options as well + +// ConnParamKey is an enum for PostgreSQL connection parameters +type ConnParamKey string + +const ( + // ConnParamKeyHost defines the key for the hostname + ConnParamKeyHost ConnParamKey = "host" + // ConnParamKeyPort defines the key for the port + ConnParamKeyPort ConnParamKey = "port" + // ConnParamKeyUser defines the key for the username + ConnParamKeyUser ConnParamKey = "user" + // ConnParamKeyPassword defines the key for the password + ConnParamKeyPassword ConnParamKey = "password" + // ConnParamKeyDbName defines the key for the database name + ConnParamKeyDbName ConnParamKey = "dbname" + // ConnParamKeyAppName defines the key for the application name + ConnParamKeyAppName ConnParamKey = "application_name" + // ConnParamKeySSLMode defines the key for the ssl mode + ConnParamKeySSLMode ConnParamKey = "sslmode" +) + +// scanner implements a tokenizer for libpq-style option strings. +type scanner struct { + s []rune + i int +} + +// newScanner returns a new scanner initialized with the option string s. +func newScanner(s string) *scanner { + return &scanner{[]rune(s), 0} +} + +// Next returns the next rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) Next() (rune, bool) { + if s.i >= len(s.s) { + return 0, false + } + r := s.s[s.i] + s.i++ + return r, true +} + +// SkipSpaces returns the next non-whitespace rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) SkipSpaces() (rune, bool) { + r, ok := s.Next() + for unicode.IsSpace(r) && ok { + r, ok = s.Next() + } + return r, ok +} + +// ParseConnString parses the options from name and adds them to the values. +// +// The parsing code is based on conninfo_parse from libpq's fe-connect.c +func ParseConnString(name string) (ConnParams, error) { + p := ConnParams{} + s := newScanner(name) + + for { + var ( + keyRunes, valRunes []rune + r rune + ok bool + ) + + if r, ok = s.SkipSpaces(); !ok { + break + } + + // Scan the key + for !unicode.IsSpace(r) && r != '=' { + keyRunes = append(keyRunes, r) + if r, ok = s.Next(); !ok { + break + } + } + + // Skip any whitespace if we're not at the = yet + if r != '=' { + r, ok = s.SkipSpaces() + } + + // The current character should be = + if r != '=' || !ok { + return nil, fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) + } + + // Skip any whitespace after the = + if r, ok = s.SkipSpaces(); !ok { + // If we reach the end here, the last value is just an empty string as per libpq. + p.Set(ConnParamKey(ConnParamKey(keyRunes)), "") + break + } + + if r != '\'' { + for !unicode.IsSpace(r) { + if r == '\\' { + if r, ok = s.Next(); !ok { + return nil, errors.New(`missing character after backslash`) + } + } + valRunes = append(valRunes, r) + + if r, ok = s.Next(); !ok { + break + } + } + } else { + quote: + for { + if r, ok = s.Next(); !ok { + return nil, errors.New(`unterminated quoted string literal in connection string`) + } + switch r { + case '\'': + break quote + case '\\': + r, _ = s.Next() + fallthrough + default: + valRunes = append(valRunes, r) + } + } + } + + p.Set(ConnParamKey(keyRunes), string(valRunes)) + } + + return p, nil +} + +// URLToConnParams creates the connParams from the url. +func URLToConnParams(urlStr string) (ConnParams, error) { + p := ConnParams{} + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + if u.Scheme != "postgres" && u.Scheme != "postgresql" { + return nil, fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + if u.User != nil { + v := u.User.Username() + p.Set("user", v) + v, _ = u.User.Password() + p.Set("password", v) + } + + if host := u.Hostname(); host != "" { + p.Set("host", host) + } + if port := u.Port(); port != "" { + p.Set("port", port) + } + + if u.Path != "" { + p.Set("dbname", u.Path[1:]) + } + + q := u.Query() + for k := range q { + p.Set(ConnParamKey(k), q.Get(k)) + } + + return p, nil +} + +var ( + // V95 represents PostgreSQL 9.5 + V95 = semver.MustParse("9.5") + // V96 represents PostgreSQL 9.6 + V96 = semver.MustParse("9.6") + // V10 represents PostgreSQL 10 + V10 = semver.MustParse("10") + // V12 represents PostgreSQL 12 + V12 = semver.MustParse("12") + // V13 represents PostgreSQL 13 + V13 = semver.MustParse("13") + // V18 represents PostgreSQL 18 + V18 = semver.MustParse("18") +) + +func parseBinaryVersion(v string) (*semver.Version, error) { + // extract version (removing beta*, rc* etc...) + + regex := regexp.MustCompile(`.* \(PostgreSQL\) ([0-9\.]+).*`) + m := regex.FindStringSubmatch(v) + if len(m) != 2 { + return nil, fmt.Errorf("failed to parse postgres binary version: %q", v) + } + return semver.NewVersion(m[1]) +} + +func parseVersion(v string) (*semver.Version, error) { + return semver.NewVersion(v) +} + +func pgDataVersion(dataDir string) (*semver.Version, error) { + fh, err := os.Open(filepath.Join(dataDir, "PG_VERSION")) + if err != nil { + return nil, fmt.Errorf("failed to read PG_VERSION: %v", err) + } + defer handledFileClose(fh) + + scanner := bufio.NewScanner(fh) + scanner.Split(bufio.ScanLines) + + scanner.Scan() + + version := scanner.Text() + return parseVersion(version) +} + +func binaryVersion(binPath string) (*semver.Version, error) { + name := filepath.Join(binPath, "postgres") + cmd := exec.Command(name, "-V") + utlLogger.Debug().Str(logCmd, cmd.String()).Msg("execing cmd") + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error: %v, output: %s", err, string(out)) + } + return parseBinaryVersion(string(out)) +} + +const ( + // TODO: can we autodetect if this is non-default? + walSegSize = (16 * 1024 * 1024) // 16MiB + globalDB = "postgres" + + urwx = 0o700 + urw = 0o600 + + logCmd = "cmd" + + base16 = 16 + base10 = 10 + bitSize32 = 32 + bitSize64 = 64 + + historyTimelineStringLength = 4 + fileNameTimelineStringLength = 8 + walNameLength = 24 +) + +var ( + validReplSlotName = regexp.MustCompile("^[a-z0-9_]+$") +) + +func handledDbClose(db *sql.DB) { + if err := db.Close(); err != nil { + utlLogger.Fatal().AnErr("err", err).Msg("Failed to close db connection") + } +} + +func handledRowsClose(rows *sql.Rows) { + if err := rows.Close(); err != nil { + utlLogger.Fatal().AnErr("err", err).Msg("Failed to close cursor") + } +} + +func handledFileClose(fh *os.File) { + if err := fh.Close(); err != nil { + utlLogger.Fatal().Str("file", fh.Name()).AnErr("err", err).Msg("Failed to close") + } +} + +func handledDBClose(fh *sql.DB) { + if err := fh.Close(); err != nil { + utlLogger.Fatal().AnErr("err", err).Msg("Failed to close database") + } +} + +func handledFileRemove(fh *os.File) { + if err := os.Remove(fh.Name()); err != nil { + utlLogger.Fatal().Str("path", fh.Name()).AnErr("err", err).Msg("Failed to remove") + } + handledFileClose(fh) +} + +func dbExec(ctx context.Context, db *sql.DB, query string, args ...any) (sql.Result, error) { + return db.ExecContext(ctx, query, args...) +} + +func query(ctx context.Context, db *sql.DB, query string, args ...any) (*sql.Rows, error) { + return db.QueryContext(ctx, query, args...) +} + +func ping(ctx context.Context, connParams ConnParams) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDBClose(db) + + _, err = dbExec(ctx, db, "select 1") + if err != nil { + return err + } + return nil +} + +func setPassword(ctx context.Context, connParams ConnParams, username, password string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + tx, err := db.Begin() + if err != nil { + return err + } + + query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + + query = fmt.Sprintf( + "alter role %s with encrypted password %s", + pq.QuoteIdentifier(username), + pq.QuoteLiteral(password), + ) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +// TODO: remove _ parameters + +func createRole(ctx context.Context, connParams ConnParams, _ []string, username, password string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + tx, err := db.Begin() + if err != nil { + return err + } + + query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + + query = fmt.Sprintf( + "create role %s with login replication encrypted password %s", + pq.QuoteIdentifier(username), + pq.QuoteLiteral(password), + ) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +func createPasswordlessRole(ctx context.Context, connParams ConnParams, _ []string, username string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + _, err = dbExec(ctx, db, fmt.Sprintf(`create role "%s" with login replication;`, username)) + return err +} + +func alterRole(ctx context.Context, connParams ConnParams, _ []string, username, password string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + tx, err := db.Begin() + if err != nil { + return err + } + + query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + + query = fmt.Sprintf( + "alter role %s with login replication encrypted password %s", + pq.QuoteIdentifier(username), + pq.QuoteLiteral(password), + ) + if _, err = tx.ExecContext(ctx, query); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +func alterPasswordlessRole(ctx context.Context, connParams ConnParams, _ []string, username string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + _, err = dbExec(ctx, db, fmt.Sprintf(`alter role "%s" with login replication;`, username)) + return err +} + +// getReplicatinSlots return existing replication slots. On PostgreSQL > 10 we +// skip temporary slots. +func getReplicationSlots(ctx context.Context, connParams ConnParams, version *semver.Version) ([]string, error) { + var q string + if version.LessThan(V10) { + q = "select slot_name from pg_replication_slots" + } else { + q = "select slot_name from pg_replication_slots where temporary is false" + } + + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return nil, err + } + defer handledDbClose(db) + + replSlots := []string{} + + rows, err := query(ctx, db, q) + if err != nil { + return nil, err + } + defer handledRowsClose(rows) + for rows.Next() { + var slotName string + if err := rows.Scan(&slotName); err != nil { + return nil, err + } + replSlots = append(replSlots, slotName) + } + + return replSlots, nil +} + +func createReplicationSlot(ctx context.Context, connParams ConnParams, name string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + _, err = dbExec(ctx, db, fmt.Sprintf("select pg_create_physical_replication_slot('%s')", name)) + return err +} + +func dropReplicationSlot(ctx context.Context, connParams ConnParams, name string) error { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return err + } + defer handledDbClose(db) + + _, err = dbExec(ctx, db, fmt.Sprintf("select pg_drop_replication_slot('%s')", name)) + return err +} + +func getSyncStandbys(ctx context.Context, connParams ConnParams) ([]string, error) { + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return nil, err + } + defer handledDbClose(db) + + rows, err := query(ctx, db, "select application_name, sync_state from pg_stat_replication") + if err != nil { + return nil, err + } + defer handledRowsClose(rows) + + syncStandbys := []string{} + for rows.Next() { + var applicationName, syncState string + if err := rows.Scan(&applicationName, &syncState); err != nil { + return nil, err + } + + if syncState == "sync" { + syncStandbys = append(syncStandbys, applicationName) + } + } + + return syncStandbys, nil +} + +// PGLsnToInt will return an uint64 representing an absolute byte in the WAL stream +func PGLsnToInt(lsn string) (uint64, error) { + parts := strings.Split(lsn, "/") + if len(parts) != 2 { + return 0, fmt.Errorf("bad pg_lsn: %s", lsn) + } + a, err := strconv.ParseUint(parts[0], base16, bitSize32) + if err != nil { + return 0, err + } + b, err := strconv.ParseUint(parts[1], base16, bitSize32) + if err != nil { + return 0, err + } + v := uint64(a)< %). +func expandRecoveryCommand(cmd, dataDir, walDir string) string { + return regexp.MustCompile(`%[dw%]`).ReplaceAllStringFunc(cmd, func(match string) string { + switch match[1] { + case 'd': + return dataDir + case 'w': + return walDir + } + + return "%" + }) +} + +func getConfigFilePGParameters(ctx context.Context, connParams ConnParams) (common.Parameters, error) { + var pgParameters = common.Parameters{} + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return nil, err + } + defer handledDbClose(db) + + // We prefer pg_file_settings since pg_settings returns archive_command = '(disabled)' + // when archive_mode is off so we'll lose its value + // Check if pg_file_settings exists (pg >= 9.5) + rows, err := query( + ctx, + db, + strings.Join([]string{ + "select 1 from information_schema.tables", + "where table_schema = 'pg_catalog' ", + "and table_name = 'pg_file_settings'", + }, "\n")) + if err != nil { + return nil, err + } + defer handledRowsClose(rows) + c := 0 + for rows.Next() { + c++ + } + usePGFileSettings := false + if c > 0 { + usePGFileSettings = true + } + + if usePGFileSettings { + // NOTE If some pg_parameters that cannot be changed without a restart + // are removed from the postgresql.conf file the view will contain some + // rows with null name and setting and the error field set to the cause. + // So we have to filter out these or the Scan will fail. + rows, err = query( + ctx, + db, + strings.Join([]string{ + "select name, setting", + "from pg_file_settings", + "where name IS NOT NULL", + "and setting IS NOT NULL", + }, "\n")) + if err != nil { + return nil, err + } + defer handledRowsClose(rows) + for rows.Next() { + var name, setting string + if err = rows.Scan(&name, &setting); err != nil { + return nil, err + } + pgParameters[name] = setting + } + return pgParameters, nil + } + + // Fallback to pg_settings + rows, err = query(ctx, db, "select name, setting, source from pg_settings") + if err != nil { + return nil, err + } + defer handledRowsClose(rows) + for rows.Next() { + var name, setting, source string + if err = rows.Scan(&name, &setting, &source); err != nil { + return nil, err + } + if source == "configuration file" { + pgParameters[name] = setting + } + } + return pgParameters, nil +} + +func isRestartRequiredUsingPendingRestart(ctx context.Context, connParams ConnParams) (bool, error) { + isRestartRequired := false + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return isRestartRequired, err + } + defer handledDbClose(db) + + rows, err := query(ctx, db, "select count(*) > 0 from pg_settings where pending_restart;") + if err != nil { + return isRestartRequired, err + } + defer handledRowsClose(rows) + if rows.Next() { + if err := rows.Scan(&isRestartRequired); err != nil { + return isRestartRequired, err + } + } + + return isRestartRequired, nil +} + +func isRestartRequiredUsingPgSettingsContext( + _ context.Context, + connParams ConnParams, + changedParams []string, +) (bool, error) { + isRestartRequired := false + db, err := sql.Open(globalDB, connParams.ConnString()) + if err != nil { + return isRestartRequired, err + } + defer handledDbClose(db) + + stmt, err := db.Prepare("select count(*) > 0 from pg_settings where context = 'postmaster' and name = ANY($1)") + + if err != nil { + return false, err + } + + rows, err := stmt.Query(pq.Array(changedParams)) + if err != nil { + return isRestartRequired, err + } + defer handledRowsClose(rows) + if rows.Next() { + if err := rows.Scan(&isRestartRequired); err != nil { + return isRestartRequired, err + } + } + + return isRestartRequired, nil +} + +// IsWalFileName checks if a file name is the name of a WAL file +func IsWalFileName(name string) bool { + walChars := "0123456789ABCDEF" + if len(name) != walNameLength { + return false + } + for _, c := range name { + ok := false + for _, v := range walChars { + if c == v { + ok = true + } + } + if !ok { + return false + } + } + return true +} + +// XlogPosToWalFileNameNoTimeline can be used to convert a WAL location to a WAL file name +func XlogPosToWalFileNameNoTimeline(xLogPos uint64) string { + id := uint32(xLogPos >> bitSize32) + offset := uint32(xLogPos) + // TODO(sgotti) for now we assume wal size is the default 16M size + seg := offset / walSegSize + return fmt.Sprintf("%08X%08X", id, seg) +} + +// WalFileNameNoTimeLine returns the absolute byte from a WAL stream that a WAL file belongs to +func WalFileNameNoTimeLine(name string) (string, error) { + if !IsWalFileName(name) { + return "", errors.New("bad wal file name") + } + return name[fileNameTimelineStringLength:walNameLength], nil +} + +func moveFile(sourcePath, destPath string) error { + // using os.Rename is faster when on same filesystem + if err := os.Rename(sourcePath, destPath); err == nil { + return nil + } + // Error. Let's try to write + inputFile, err := os.Open(sourcePath) + if err != nil { + return fmt.Errorf("Couldn't open source file: %s", err) + } + inFileStat, err := inputFile.Stat() + if err != nil { + return err + } + flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC + perm := inFileStat.Mode() & os.ModePerm + outputFile, err := os.OpenFile(destPath, flag, perm) + if err != nil { + return err + } + defer handledFileClose(outputFile) + _, err = io.Copy(outputFile, inputFile) + handledFileClose(inputFile) + if err != nil { + return fmt.Errorf("Writing to output file failed: %s", err) + } + // The copy was successful, so now delete the original file + err = os.Remove(sourcePath) + if err != nil { + return fmt.Errorf("Failed removing original file: %s", err) + } + return nil +} + +func moveDirRecursive(ctx context.Context, src string, dest string) (err error) { + _, logger := logging.GetLogComponent(ctx, logging.PgComponent) + var stat fs.FileInfo + logger.Info().Str("src", src).Str("dest", dest).Msg("Moving") + if stat, err = os.Stat(src); err != nil { + logger.Error().Str("path", src).AnErr("err", err).Msg("could not get stat of file") + return err + } else if !stat.IsDir() { + return moveFile(src, dest) + } + // Make the dir if it doesn't exist + if _, err = os.Stat(dest); errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(dest, stat.Mode()&os.ModePerm); err != nil { + return err + } + } else if err != nil { + logger.Error().Str("path", dest).AnErr("err", err).Msg("could not get stat of file") + return err + } + // Copy all files and folders in this folder + var entries []fs.DirEntry + if entries, err = os.ReadDir(src); err != nil { + logger.Error().Str("path", src).AnErr("err", err).Msg("could not read contents of folder") + return err + } + for _, entry := range entries { + srcEntry := filepath.Join(src, entry.Name()) + dstEntry := filepath.Join(dest, entry.Name()) + if err := moveDirRecursive(ctx, srcEntry, dstEntry); err != nil { + return err + } + } + + // Remove this folder, which is now supposedly empty + if err := syscall.Rmdir(src); err != nil { + logger.Error().Str("path", src).AnErr("err", err).Msg("could not remove folder") + // If this is a mountpoint or you don't have enough permissions, you might nog be able to. But that is fine. + // return err + } + return nil +} diff --git a/internal/postgresql/postgresql.go b/internal/postgresql/manager.go similarity index 55% rename from internal/postgresql/postgresql.go rename to internal/postgresql/manager.go index 00c14bcd7..6685f5806 100644 --- a/internal/postgresql/postgresql.go +++ b/internal/postgresql/manager.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,55 +18,32 @@ package postgresql import ( "bufio" "context" + "encoding/binary" "errors" "fmt" "io" - "io/ioutil" + "io/fs" "os" "os/exec" "path/filepath" - "reflect" "sort" "strconv" "strings" "syscall" "time" - "github.com/sorintlab/stolon/internal/common" - slog "github.com/sorintlab/stolon/internal/log" + "github.com/Masterminds/semver/v3" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/logging" - _ "github.com/lib/pq" "github.com/mitchellh/copystructure" - "go.uber.org/zap" ) -//go:generate mockgen -destination=../mock/postgresql/postgresql.go -package=mocks -source=$GOFILE - -const ( - postgresConf = "postgresql.conf" - postgresRecoveryConf = "recovery.conf" - postgresStandbySignal = "standby.signal" - postgresRecoverySignal = "recovery.signal" - postgresRecoveryDone = "recovery.done" - postgresAutoConf = "postgresql.auto.conf" - tmpPostgresConf = "stolon-temp-postgresql.conf" - - startTimeout = 60 * time.Second -) - -var ( - ErrUnknownState = errors.New("unknown postgres state") -) - -var log = slog.S() - -type PGManager interface { - GetTimelinesHistory(timeline uint64) ([]*TimelineHistory, error) -} - +// Manager manages a PostgreSQL instance type Manager struct { pgBinPath string dataDir string + walDir string parameters common.Parameters recoveryOptions *RecoveryOptions hba []string @@ -83,60 +61,19 @@ type Manager struct { requestTimeout time.Duration } -type RecoveryMode int - -const ( - RecoveryModeNone RecoveryMode = iota - RecoveryModeStandby - RecoveryModeRecovery -) - -type RecoveryOptions struct { - RecoveryMode RecoveryMode - RecoveryParameters common.Parameters -} - -func NewRecoveryOptions() *RecoveryOptions { - return &RecoveryOptions{RecoveryParameters: make(common.Parameters)} -} - -func (r *RecoveryOptions) DeepCopy() *RecoveryOptions { - nr, err := copystructure.Copy(r) - if err != nil { - panic(err) - } - if !reflect.DeepEqual(r, nr) { - panic("not equal") - } - return nr.(*RecoveryOptions) -} - -type SystemData struct { - SystemID string - TimelineID uint64 - XLogPos uint64 -} - -type TimelineHistory struct { - TimelineID uint64 - SwitchPoint uint64 - Reason string -} - -type InitConfig struct { - Locale string - Encoding string - DataChecksums bool -} - -func SetLogger(l *zap.SugaredLogger) { - log = l -} - -func NewManager(pgBinPath string, dataDir string, localConnParams, replConnParams ConnParams, suAuthMethod, suUsername, suPassword, replAuthMethod, replUsername, replPassword string, requestTimeout time.Duration) *Manager { +// NewManager returns a freshly initialized manager resource +func NewManager( + pgBinPath string, dataDir, + walDir string, + localConnParams, + replConnParams ConnParams, + suAuthMethod, suUsername, suPassword, replAuthMethod, replUsername, replPassword string, + requestTimeout time.Duration, +) *Manager { return &Manager{ pgBinPath: pgBinPath, dataDir: filepath.Join(dataDir, "postgres"), + walDir: walDir, parameters: make(common.Parameters), recoveryOptions: NewRecoveryOptions(), curParameters: make(common.Parameters), @@ -153,14 +90,17 @@ func NewManager(pgBinPath string, dataDir string, localConnParams, replConnParam } } +// SetParameters sets PostgreSQL parameters func (p *Manager) SetParameters(parameters common.Parameters) { p.parameters = parameters } +// CurParameters returns currently set PostgreSQL parameters func (p *Manager) CurParameters() common.Parameters { return p.curParameters } +// SetRecoveryOptions sets recovery options (when applicable) func (p *Manager) SetRecoveryOptions(recoveryOptions *RecoveryOptions) { if recoveryOptions == nil { p.recoveryOptions = NewRecoveryOptions() @@ -170,57 +110,73 @@ func (p *Manager) SetRecoveryOptions(recoveryOptions *RecoveryOptions) { p.recoveryOptions = recoveryOptions } +// CurRecoveryOptions returns recovery options currently set func (p *Manager) CurRecoveryOptions() *RecoveryOptions { return p.curRecoveryOptions } +// SetHba sets HBA rules func (p *Manager) SetHba(hba []string) { p.hba = hba } +// CurHba returns HBA rules currently set func (p *Manager) CurHba() []string { return p.curHba } +// UpdateCurParameters updates the parameters func (p *Manager) UpdateCurParameters() { - n, err := copystructure.Copy(p.parameters) - if err != nil { + var ok bool + if n, err := copystructure.Copy(p.parameters); err != nil { panic(err) + } else if p.curParameters, ok = n.(common.Parameters); !ok { + panic("type is different after copy") } - p.curParameters = n.(common.Parameters) } +// UpdateCurRecoveryOptions updates recovery options to new values func (p *Manager) UpdateCurRecoveryOptions() { p.curRecoveryOptions = p.recoveryOptions.DeepCopy() } +// UpdateCurHba will update HBA rules func (p *Manager) UpdateCurHba() { - n, err := copystructure.Copy(p.hba) - if err != nil { + var ok bool + if n, err := copystructure.Copy(p.hba); err != nil { panic(err) + } else if p.curHba, ok = n.([]string); !ok { + panic("type is different after copy") } - p.curHba = n.([]string) } -func (p *Manager) Init(initConfig *InitConfig) error { - // ioutil.Tempfile already creates files with 0600 permissions - pwfile, err := ioutil.TempFile("", "pwfile") +// Init will initialize a PostgreSQL data directory +func (p *Manager) Init(ctx context.Context, initConfig *InitConfig) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + // os.CreateTemp already creates files with urw permissions + pwfile, err := os.CreateTemp("", "pwfile") if err != nil { return err } - defer os.Remove(pwfile.Name()) - defer pwfile.Close() + defer handledFileRemove(pwfile) if _, err = pwfile.WriteString(p.suPassword); err != nil { return err } name := filepath.Join(p.pgBinPath, "initdb") - cmd := exec.Command(name, "-D", p.dataDir, "-U", p.suUsername) + cmd := exec.Command(name, argDatadir, p.dataDir, "-U", p.suUsername) if p.suAuthMethod == "md5" { cmd.Args = append(cmd.Args, "--pwfile", pwfile.Name()) } - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) + + // initdb supports configuring a separate wal directory via symlinks. Normally this + // parameter might be part of the initConfig, but it will also be required whenever we + // fall-back to a pg_basebackup during a re-sync, which is why it's a Manager field. + if p.walDir != "" { + cmd.Args = append(cmd.Args, "--waldir", p.walDir) + } if initConfig.Locale != "" { cmd.Args = append(cmd.Args, "--locale", initConfig.Locale) @@ -240,24 +196,28 @@ func (p *Manager) Init(initConfig *InitConfig) error { } // remove the dataDir, so we don't end with an half initialized database if err != nil { - os.RemoveAll(p.dataDir) + if cleanupErr := p.RemoveAll(ctx); cleanupErr != nil { + logger.Error().AnErr("err", cleanupErr).Msg("failed to cleanup database") + } return err } return nil } -func (p *Manager) Restore(command string) error { +// Restore will run a restore command +func (p *Manager) Restore(ctx context.Context, command string) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) var err error var cmd *exec.Cmd - command = expand(command, p.dataDir) + command = expandRecoveryCommand(command, p.dataDir, p.walDir) - if err = os.MkdirAll(p.dataDir, 0700); err != nil { + if err = os.MkdirAll(p.dataDir, urwx); err != nil { err = fmt.Errorf("cannot create data dir: %v", err) goto out } cmd = exec.Command("/bin/sh", "-c", command) - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg("executing cmd") // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout @@ -267,9 +227,13 @@ func (p *Manager) Restore(command string) error { goto out } // On every error remove the dataDir, so we don't end with an half initialized database + + // TODO: replace goto with defer out: if err != nil { - os.RemoveAll(p.dataDir) + if cleanupErr := p.RemoveAll(ctx); cleanupErr != nil { + logger.Error().AnErr("err", cleanupErr).Msg("failed to cleanup database") + } return err } return nil @@ -277,20 +241,126 @@ out: // StartTmpMerged starts postgres with a conf file different than // postgresql.conf, including it at the start of the conf if it exists -func (p *Manager) StartTmpMerged() error { - if err := p.writeConfs(true); err != nil { +func (p *Manager) StartTmpMerged(ctx context.Context) error { + if err := p.writeConfs(ctx, true); err != nil { return err } tmpPostgresConfPath := filepath.Join(p.dataDir, tmpPostgresConf) - return p.start("-c", fmt.Sprintf("config_file=%s", tmpPostgresConfPath)) + return p.start(ctx, "-c", fmt.Sprintf("config_file=%s", tmpPostgresConfPath)) +} + +func (p *Manager) moveWal(ctx context.Context) (err error) { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + var curPath string + var desiredPath string + var tmpPath string + symlinkPath := filepath.Join(p.dataDir, "pg_wal") + if curPath, err = filepath.EvalSymlinks(symlinkPath); err != nil { + logger.Error().Str("path", symlinkPath).AnErr("err", err).Msg("could not evaluate symlink") + return err + } + if p.walDir == "" { + desiredPath = symlinkPath + tmpPath = filepath.Join(p.dataDir, "pg_wal_new") + } else { + desiredPath = p.walDir + tmpPath = p.walDir + } + if curPath == desiredPath { + return nil + } + if p.walDir == "" { + logger.Info(). + Str("src", curPath). + Str("temp path", tmpPath). + Str("dest", desiredPath). + Msg("moving WAL from src to temp path first and then to dest") + } else { + logger.Info(). + Str("src", curPath). + Str("dest", desiredPath). + Msg("moving WAL from src to dest") + } + // We use tmpPath here first and (if needed) mv tmpPath to desiredPath when all is copied. + // This allows stolon-keeper to re-read symlink dest and continue should stolon-keeper be restarted while copying. + if err = moveDirRecursive(ctx, curPath, tmpPath); err != nil { + return err + } + + var symlinkStat fs.FileInfo + if symlinkStat, err = os.Lstat(symlinkPath); errors.Is(err, os.ErrNotExist) { + logger.Debug().Msg("file or folder already removed") + } else if err != nil { + logger.Error(). + Str("path", symlinkPath). + AnErr("err", err). + Msg("could not get info on current pg_wal folder/symlink path") + return err + } else if symlinkStat.Mode()&os.ModeSymlink != 0 { + if err = os.Remove(symlinkPath); err != nil { + logger.Error(). + Str("path", symlinkPath). + AnErr("err", err). + Msg("could not remove current pg_wal symlink path") + return err + } + } else if symlinkStat.IsDir() { + if err = syscall.Rmdir(symlinkPath); err != nil { + logger.Error(). + Str("path", symlinkPath). + AnErr("err", err). + Msg("could not remove current folder") + return err + } + } else { + err = fmt.Errorf("location %s is no symlink and no dir, so please check and resolve by hand", symlinkPath) + logger.Error().AnErr("err", err).Msg("") + return err + } + if p.walDir == "" { + // So we were moving WAL files back into PGDATA. Let's rename the tmpDir now holding all WAL files and use that + // as PGDATA/pg_wal + if err = os.Rename(tmpPath, desiredPath); err != nil { + logger.Error(). + Str("temp path", tmpPath). + Str("dest", desiredPath). + AnErr("err", err). + Msg("cannot move temp path to dest") + return err + } + } else { + logger.Info(). + Str("src", symlinkPath). + Str("dest", desiredPath). + Msg("symlinking src to dest") + if err = os.Symlink(desiredPath, symlinkPath); err != nil { + // We were copying WAL files from PGDATA (or another location) to a location outside of PGDATA and + // pointing the symlink in the right direction failed. + logger.Error(). + Str("symlink", symlinkPath). + Str("dest", desiredPath). + AnErr("err", err). + Msg("could not create symlink to dest") + return err + } + } + logger.Info(). + Str("src", curPath). + Str("dest", desiredPath). + Msg("moving pg_wal is successful") + return nil } -func (p *Manager) Start() error { - if err := p.writeConfs(false); err != nil { +// Start will start the PostgreSQL instance +func (p *Manager) Start(ctx context.Context) error { + if err := p.writeConfs(ctx, false); err != nil { + return err + } + if err := p.moveWal(ctx); err != nil { return err } - return p.start() + return p.start(ctx) } // start starts the instance. A success means that the instance has been @@ -298,7 +368,7 @@ func (p *Manager) Start() error { // connections (i.e. it's waiting for some missing wals etc...). // Note that also on error an instance may still be active and, if needed, // should be manually stopped calling Stop. -func (p *Manager) start(args ...string) error { +func (p *Manager) start(ctx context.Context, args ...string) error { // pg_ctl for postgres < 10 with -w will exit after the timeout and return 0 // also if the instance isn't ready to accept connections, while for // postgres >= 10 it will return a non 0 exit code making it impossible to @@ -312,15 +382,17 @@ func (p *Manager) start(args ...string) error { // the instance parent is the keeper instead of the defined system reaper // (since pg_ctl forks and then exits leaving the postmaster orphaned). + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) if err := p.createPostgresqlAutoConf(); err != nil { return err } - log.Infow("starting database") + logger.Info().Msg("starting database") name := filepath.Join(p.pgBinPath, "postgres") - args = append([]string{"-D", p.dataDir, "-c", "unix_socket_directories=" + common.PgUnixSocketDirectories}, args...) + args = append([]string{argDatadir, p.dataDir, "-c", + "unix_socket_directories=" + common.PgUnixSocketDirectories}, args...) cmd := exec.Command(name, args...) - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -343,7 +415,13 @@ func (p *Manager) start(args ...string) error { ok := false start := time.Now() for time.Since(start) < startTimeout { - fh, err := os.Open(filepath.Join(p.dataDir, "postmaster.pid")) + fileName := filepath.Join(p.dataDir, "postmaster.pid") + fh, err := os.Open(fileName) + defer func() { + if err := fh.Close(); err != nil { + logger.Debug().Msgf("failed to close %s: %v", fileName, err) + } + }() if err == nil { scanner := bufio.NewScanner(fh) scanner.Split(bufio.ScanLines) @@ -351,16 +429,14 @@ func (p *Manager) start(args ...string) error { fpid := scanner.Text() if fpid == strconv.Itoa(pid) { ok = true - fh.Close() break } } } - fh.Close() select { case <-exited: - return fmt.Errorf("postgres exited unexpectedly") + return errors.New("postgres exited unexpectedly") default: } @@ -368,7 +444,7 @@ func (p *Manager) start(args ...string) error { } if !ok { - return fmt.Errorf("instance still starting") + return errors.New("instance still starting") } p.UpdateCurParameters() @@ -380,14 +456,16 @@ func (p *Manager) start(args ...string) error { // Stop tries to stop an instance. An error will be returned if the instance isn't started, stop fails or // times out (60 second). -func (p *Manager) Stop(fast bool) error { - log.Infow("stopping database") +func (p *Manager) Stop(ctx context.Context, fast bool) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + logger.Info().Msg("stopping database") name := filepath.Join(p.pgBinPath, "pg_ctl") - cmd := exec.Command(name, "stop", "-w", "-D", p.dataDir, "-o", "-c unix_socket_directories="+common.PgUnixSocketDirectories) + cmd := exec.Command(name, "stop", "-w", argDatadir, p.dataDir, "-o", + "-c unix_socket_directories="+common.PgUnixSocketDirectories) if fast { cmd.Args = append(cmd.Args, "-m", "fast") } - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout @@ -398,18 +476,20 @@ func (p *Manager) Stop(fast bool) error { return nil } +// IsStarted will return true if PostgreSQL is currently running func (p *Manager) IsStarted() (bool, error) { name := filepath.Join(p.pgBinPath, "pg_ctl") - cmd := exec.Command(name, "status", "-D", p.dataDir, "-o", "-c unix_socket_directories="+common.PgUnixSocketDirectories) + cmd := exec.Command(name, "status", argDatadir, p.dataDir, "-o", + "-c unix_socket_directories="+common.PgUnixSocketDirectories) _, err := cmd.CombinedOutput() if err != nil { if _, ok := err.(*exec.ExitError); ok { status := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() - if status == 3 { + if status == exitStatusNotRunning { return false, nil } - if status == 4 { - return false, ErrUnknownState + if status == exitStatusInaccessibleDatadir { + return false, ErrInaccessibleDatadir } } return false, fmt.Errorf("cannot get instance state: %v", err) @@ -417,16 +497,19 @@ func (p *Manager) IsStarted() (bool, error) { return true, nil } -func (p *Manager) Reload() error { - log.Infow("reloading database configuration") +// Reload will trigger a reload in PostgreSQL +func (p *Manager) Reload(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + logger.Info().Msg("reloading database configuration") - if err := p.writeConfs(false); err != nil { + if err := p.writeConfs(ctx, false); err != nil { return err } name := filepath.Join(p.pgBinPath, "pg_ctl") - cmd := exec.Command(name, "reload", "-D", p.dataDir, "-o", "-c unix_socket_directories="+common.PgUnixSocketDirectories) - log.Debugw("execing cmd", "cmd", cmd) + cmd := exec.Command(name, "reload", argDatadir, p.dataDir, "-o", + "-c unix_socket_directories="+common.PgUnixSocketDirectories) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout @@ -444,12 +527,12 @@ func (p *Manager) Reload() error { // StopIfStarted checks if the instance is started, then calls stop and // then check if the instance is really stopped -func (p *Manager) StopIfStarted(fast bool) error { +func (p *Manager) StopIfStarted(ctx context.Context, fast bool) error { // Stop will return an error if the instance isn't started, so first check // if it's started started, err := p.IsStarted() if err != nil { - if err == ErrUnknownState { + if err == ErrInaccessibleDatadir { // if IsStarted returns an unknown state error then assume that the // instance is stopped return nil @@ -459,7 +542,7 @@ func (p *Manager) StopIfStarted(fast bool) error { if !started { return nil } - if err = p.Stop(fast); err != nil { + if err = p.Stop(ctx, fast); err != nil { return err } started, err = p.IsStarted() @@ -467,41 +550,42 @@ func (p *Manager) StopIfStarted(fast bool) error { return err } if started { - return fmt.Errorf("failed to stop") + return errors.New("failed to stop") } return nil } -func (p *Manager) Restart(fast bool) error { - log.Infow("restarting database") - if err := p.StopIfStarted(fast); err != nil { - return err - } - if err := p.Start(); err != nil { +// Restart will stop (if started) and start PostgreSQL +func (p *Manager) Restart(ctx context.Context, fast bool) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + logger.Info().Msg("restarting database") + if err := p.StopIfStarted(ctx, fast); err != nil { return err } - return nil + return p.Start(ctx) } -func (p *Manager) WaitReady(timeout time.Duration) error { +// WaitReady will wait for PostgreSQL to be available +func (p *Manager) WaitReady(ctx context.Context, timeout time.Duration) error { start := time.Now() for timeout == 0 || time.Since(start) < timeout { - if err := p.Ping(); err == nil { + if err := p.Ping(ctx); err == nil { return nil } time.Sleep(200 * time.Millisecond) } - return fmt.Errorf("timeout waiting for db ready") + return errors.New("timeout waiting for db ready") } +// WaitRecoveryDone will wait for recovery to be done (signal or done file) func (p *Manager) WaitRecoveryDone(timeout time.Duration) error { - maj, _, err := p.BinaryVersion() + version, err := p.BinaryVersion() if err != nil { return fmt.Errorf("error fetching pg version: %v", err) } start := time.Now() - if maj >= 12 { + if version.GreaterThanEqual(V12) { for timeout == 0 || time.Since(start) < timeout { _, err := os.Stat(filepath.Join(p.dataDir, postgresRecoverySignal)) if err != nil && !os.IsNotExist(err) { @@ -525,15 +609,27 @@ func (p *Manager) WaitRecoveryDone(timeout time.Duration) error { } } - return fmt.Errorf("timeout waiting for db recovery") + return errors.New("timeout waiting for db recovery") } -func (p *Manager) Promote() error { - log.Infow("promoting database") +// PGDataVersion returns the version of the PostgreSQL data directory +func (p *Manager) PGDataVersion() (*semver.Version, error) { + return pgDataVersion(p.dataDir) +} + +// BinaryVersion returns the version of the PostgreSQL binaries +func (p *Manager) BinaryVersion() (*semver.Version, error) { + return binaryVersion(p.pgBinPath) +} + +// Promote will promote PostgreSQL (with pg_ctl) +func (p *Manager) Promote(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + logger.Info().Msg("promoting database") name := filepath.Join(p.pgBinPath, "pg_ctl") - cmd := exec.Command(name, "promote", "-w", "-D", p.dataDir) - log.Debugw("execing cmd", "cmd", cmd) + cmd := exec.Command(name, "promote", "-w", argDatadir, p.dataDir) + logger.Debug().Str(logCmd, cmd.String()).Msg("executing cmd") // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout @@ -542,40 +638,44 @@ func (p *Manager) Promote() error { return fmt.Errorf("error: %v", err) } - if err := p.writeConfs(false); err != nil { - return err - } - - return nil + return p.writeConfs(ctx, false) } -func (p *Manager) SetupRoles() error { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// SetupRoles will connect to PostgreSQL to setup all users as required by stolon +func (p *Manager) SetupRoles(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() if p.suUsername == p.replUsername { - log.Infow("adding replication role to superuser") + logger.Info().Msg("adding replication role to superuser") if p.suAuthMethod == "trust" { if err := alterPasswordlessRole(ctx, p.localConnParams, []string{"replication"}, p.suUsername); err != nil { return fmt.Errorf("error adding replication role to superuser: %v", err) } } else { - if err := alterRole(ctx, p.localConnParams, []string{"replication"}, p.suUsername, p.suPassword); err != nil { + if err := alterRole( + ctx, + p.localConnParams, + []string{"replication"}, + p.suUsername, + p.suPassword, + ); err != nil { return fmt.Errorf("error adding replication role to superuser: %v", err) } } - log.Infow("replication role added to superuser") + logger.Info().Msg("replication role added to superuser") } else { // Configure superuser role password if auth method is not trust if p.suAuthMethod != "trust" && p.suPassword != "" { - log.Infow("setting superuser password") + logger.Info().Msg("setting superuser password") if err := setPassword(ctx, p.localConnParams, p.suUsername, p.suPassword); err != nil { return fmt.Errorf("error setting superuser password: %v", err) } - log.Infow("superuser password set") + logger.Info().Msg("superuser password set") } roles := []string{"login", "replication"} - log.Infow("creating replication role") + logger.Info().Msg("creating replication role") if p.replAuthMethod != "trust" { if err := createRole(ctx, p.localConnParams, roles, p.replUsername, p.replPassword); err != nil { return fmt.Errorf("error creating replication role: %v", err) @@ -585,68 +685,45 @@ func (p *Manager) SetupRoles() error { return fmt.Errorf("error creating replication role: %v", err) } } - log.Infow("replication role created", "role", p.replUsername) + logger.Info().Str("role", p.replUsername).Msg("replication role created") } return nil } -func (p *Manager) GetSyncStandbys() ([]string, error) { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// GetSyncStandbys returns the standby's (from pg_stat_replication) +func (p *Manager) GetSyncStandbys(ctx context.Context) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return getSyncStandbys(ctx, p.localConnParams) } -func (p *Manager) GetReplicationSlots() ([]string, error) { - maj, _, err := p.PGDataVersion() +// GetReplicationSlots (from pg_replication_slots) +func (p *Manager) GetReplicationSlots(ctx context.Context) ([]string, error) { + version, err := p.PGDataVersion() if err != nil { return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() - return getReplicationSlots(ctx, p.localConnParams, maj) + return getReplicationSlots(ctx, p.localConnParams, version) } -func (p *Manager) CreateReplicationSlot(name string) error { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// CreateReplicationSlot will create a replication slot +func (p *Manager) CreateReplicationSlot(ctx context.Context, name string) error { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return createReplicationSlot(ctx, p.localConnParams, name) } -func (p *Manager) DropReplicationSlot(name string) error { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// DropReplicationSlot will drop a replication slot +func (p *Manager) DropReplicationSlot(ctx context.Context, name string) error { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return dropReplicationSlot(ctx, p.localConnParams, name) } -func (p *Manager) BinaryVersion() (int, int, error) { - name := filepath.Join(p.pgBinPath, "postgres") - cmd := exec.Command(name, "-V") - log.Debugw("execing cmd", "cmd", cmd) - out, err := cmd.CombinedOutput() - if err != nil { - return 0, 0, fmt.Errorf("error: %v, output: %s", err, string(out)) - } - - return ParseBinaryVersion(string(out)) -} - -func (p *Manager) PGDataVersion() (int, int, error) { - fh, err := os.Open(filepath.Join(p.dataDir, "PG_VERSION")) - if err != nil { - return 0, 0, fmt.Errorf("failed to read PG_VERSION: %v", err) - } - defer fh.Close() - - scanner := bufio.NewScanner(fh) - scanner.Split(bufio.ScanLines) - - scanner.Scan() - - version := scanner.Text() - return ParseVersion(version) -} - +// IsInitialized checks if a datadirectory is already initialized func (p *Manager) IsInitialized() (bool, error) { // List of required files or directories relative to postgres data dir // From https://www.postgresql.org/docs/9.4/static/storage-file-layout.html @@ -660,7 +737,7 @@ func (p *Manager) IsInitialized() (bool, error) { if !exists { return false, nil } - maj, _, err := p.PGDataVersion() + version, err := p.PGDataVersion() if err != nil { return false, err } @@ -684,7 +761,7 @@ func (p *Manager) IsInitialized() (bool, error) { } // in postgres 10 pc_clog has been renamed to pg_xact and pc_xlog has been // renamed to pg_wal - if maj < 10 { + if version.LessThan(V10) { requiredFiles = append(requiredFiles, []string{ "pg_clog", "pg_xlog", @@ -694,7 +771,6 @@ func (p *Manager) IsInitialized() (bool, error) { "pg_xact", "pg_wal", }...) - } for _, f := range requiredFiles { exists, err := fileExists(filepath.Join(p.dataDir, f)) @@ -710,42 +786,41 @@ func (p *Manager) IsInitialized() (bool, error) { // GetRole return the current instance role func (p *Manager) GetRole() (common.Role, error) { - maj, _, err := p.BinaryVersion() + version, err := p.BinaryVersion() if err != nil { return "", fmt.Errorf("error fetching pg version: %v", err) } - if maj >= 12 { + if version.GreaterThanEqual(V12) { // if standby.signal file exists then consider it as a standby _, err := os.Stat(filepath.Join(p.dataDir, postgresStandbySignal)) if err != nil && !os.IsNotExist(err) { return "", fmt.Errorf("error determining if %q file exists: %v", postgresStandbySignal, err) } if os.IsNotExist(err) { - return common.RoleMaster, nil - } - return common.RoleStandby, nil - } else { - // if recovery.conf file exists then consider it as a standby - _, err := os.Stat(filepath.Join(p.dataDir, postgresRecoveryConf)) - if err != nil && !os.IsNotExist(err) { - return "", fmt.Errorf("error determining if %q file exists: %v", postgresRecoveryConf, err) - } - if os.IsNotExist(err) { - return common.RoleMaster, nil + return common.RolePrimary, nil } - return common.RoleStandby, nil + return common.RoleReplica, nil + } + // if recovery.conf file exists then consider it as a standby + _, err = os.Stat(filepath.Join(p.dataDir, postgresRecoveryConf)) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("error determining if %q file exists: %v", postgresRecoveryConf, err) + } + if os.IsNotExist(err) { + return common.RolePrimary, nil } + return common.RoleReplica, nil } -func (p *Manager) writeConfs(useTmpPostgresConf bool) error { - maj, _, err := p.BinaryVersion() +func (p *Manager) writeConfs(ctx context.Context, useTmpPostgresConf bool) error { + version, err := p.BinaryVersion() if err != nil { return fmt.Errorf("error fetching pg version: %v", err) } writeRecoveryParamsInPostgresConf := false - if maj >= 12 { + if version.GreaterThanEqual(V12) { writeRecoveryParamsInPostgresConf = true } @@ -760,10 +835,10 @@ func (p *Manager) writeConfs(useTmpPostgresConf bool) error { return fmt.Errorf("error writing %s file: %v", postgresRecoveryConf, err) } } else { - if err := p.writeStandbySignal(); err != nil { + if err := p.writeStandbySignal(ctx); err != nil { return fmt.Errorf("error writing %s file: %v", postgresStandbySignal, err) } - if err := p.writeRecoverySignal(); err != nil { + if err := p.writeRecoverySignal(ctx); err != nil { return fmt.Errorf("error writing %s file: %v", postgresRecoverySignal, err) } } @@ -776,7 +851,7 @@ func (p *Manager) writeConf(useTmpPostgresConf, writeRecoveryParams bool) error confFile = tmpPostgresConf } - return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, confFile), 0600, + return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, confFile), urw, func(f io.Writer) error { if useTmpPostgresConf { // include postgresql.conf if it exists @@ -792,7 +867,7 @@ func (p *Manager) writeConf(useTmpPostgresConf, writeRecoveryParams bool) error } for k, v := range p.parameters { // Single quotes needs to be doubled - ev := strings.Replace(v, `'`, `''`, -1) + ev := strings.Replace(v, "'", "''", -1) if _, err := f.Write([]byte(fmt.Sprintf("%s = '%s'\n", k, ev))); err != nil { return err } @@ -819,7 +894,7 @@ func (p *Manager) writeRecoveryConf() error { return nil } - return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresRecoveryConf), 0600, + return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresRecoveryConf), urw, func(f io.Writer) error { if p.recoveryOptions.RecoveryMode == RecoveryModeStandby { if _, err := f.Write([]byte("standby_mode = 'on'\n")); err != nil { @@ -835,36 +910,38 @@ func (p *Manager) writeRecoveryConf() error { }) } -func (p *Manager) writeStandbySignal() error { +func (p *Manager) writeStandbySignal(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) // write standby.signal only if recoveryMode is standby if p.recoveryOptions.RecoveryMode != RecoveryModeStandby { return nil } - log.Infof("writing standby signal file") + logger.Info().Msg("writing standby signal file") - return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresStandbySignal), 0600, - func(f io.Writer) error { + return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresStandbySignal), urw, + func(_ io.Writer) error { return nil }) } -func (p *Manager) writeRecoverySignal() error { +func (p *Manager) writeRecoverySignal(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) // write standby.signal only if recoveryMode is recovery if p.recoveryOptions.RecoveryMode != RecoveryModeRecovery { return nil } - log.Infof("writing recovery signal file") + logger.Info().Msg("writing recovery signal file") - return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresRecoverySignal), 0600, - func(f io.Writer) error { + return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, postgresRecoverySignal), urw, + func(_ io.Writer) error { return nil }) } func (p *Manager) writePgHba() error { - return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, "pg_hba.conf"), 0600, + return common.WriteFileAtomicFunc(filepath.Join(p.dataDir, "pg_hba.conf"), urw, func(f io.Writer) error { if p.hba != nil { for _, e := range p.hba { @@ -890,20 +967,21 @@ func (p *Manager) createPostgresqlAutoConf() error { return nil } -func (p *Manager) SyncFromFollowedPGRewind(followedConnParams ConnParams, password string) error { +// SyncFromFollowedPGRewind runs pgrewind to get to a shared point in the WAL stream +func (p *Manager) SyncFromFollowedPGRewind(ctx context.Context, followedConnParams ConnParams, password string) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) // Remove postgresql.auto.conf since pg_rewind will error if it's a symlink to /dev/null pgAutoConfPath := filepath.Join(p.dataDir, postgresAutoConf) if err := os.Remove(pgAutoConfPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("error removing postgresql.auto.conf file: %v", err) } - // ioutil.Tempfile already creates files with 0600 permissions - pgpass, err := ioutil.TempFile("", "pgpass") + // os.CreateTemp already creates files with urw permissions + pgpass, err := os.CreateTemp("", "pgpass") if err != nil { return err } - defer os.Remove(pgpass.Name()) - defer pgpass.Close() + defer handledFileRemove(pgpass) host := followedConnParams.Get("host") port := followedConnParams.Get("port") @@ -918,11 +996,11 @@ func (p *Manager) SyncFromFollowedPGRewind(followedConnParams ConnParams, passwo followedConnParams.Set("options", "-c synchronous_commit=off") followedConnString := followedConnParams.ConnString() - log.Infow("running pg_rewind") + logger.Info().Msg("running pg_rewind") name := filepath.Join(p.pgBinPath, "pg_rewind") - cmd := exec.Command(name, "--debug", "-D", p.dataDir, "--source-server="+followedConnString) + cmd := exec.Command(name, "--debug", argDatadir, p.dataDir, "--source-server="+followedConnString) cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSFILE=%s", pgpass.Name())) - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) // Pipe command's std[err|out] to parent. cmd.Stdout = os.Stdout @@ -933,16 +1011,17 @@ func (p *Manager) SyncFromFollowedPGRewind(followedConnParams ConnParams, passwo return nil } -func (p *Manager) SyncFromFollowed(followedConnParams ConnParams, replSlot string) error { +// SyncFromFollowed runs pg_basebackup to resync a broken replica +func (p *Manager) SyncFromFollowed(ctx context.Context, followedConnParams ConnParams, replSlot string) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) fcp := followedConnParams.Copy() - // ioutil.Tempfile already creates files with 0600 permissions - pgpass, err := ioutil.TempFile("", "pgpass") + // already creates files with urw permissions + pgpass, err := os.CreateTemp("", "pgpass") if err != nil { return err } - defer os.Remove(pgpass.Name()) - defer pgpass.Close() + defer handledFileRemove(pgpass) host := fcp.Get("host") port := fcp.Get("port") @@ -961,16 +1040,19 @@ func (p *Manager) SyncFromFollowed(followedConnParams ConnParams, replSlot strin fcp.Set("options", "-c synchronous_commit=off") followedConnString := fcp.ConnString() - log.Infow("running pg_basebackup") + logger.Info().Msg("running pg_basebackup") name := filepath.Join(p.pgBinPath, "pg_basebackup") - args := []string{"-R", "-v", "-P", "-Xs", "-D", p.dataDir, "-d", followedConnString} + args := []string{"-R", "-v", "-P", "-Xs", argDatadir, p.dataDir, argDbName, followedConnString} if replSlot != "" { args = append(args, "--slot", replSlot) } + if p.walDir != "" { + args = append(args, "--waldir", p.walDir) + } cmd := exec.Command(name, args...) cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSFILE=%s", pgpass.Name())) - log.Debugw("execing cmd", "cmd", cmd) + logger.Debug().Str(logCmd, cmd.String()).Msg(logExec) // Pipe pg_basebackup's stderr to our stderr. // We do this indirectly so that pg_basebackup doesn't think it's connected to a tty. @@ -990,17 +1072,15 @@ func (p *Manager) SyncFromFollowed(followedConnParams ConnParams, replSlot strin go func() { if _, err := io.Copy(os.Stderr, stderr); err != nil { - log.Errorf("pg_basebackup failed to copy stderr: %v", err) + logger.Error().AnErr("err", err).Msg("pg_basebackup failed to copy stderr") } }() - if err := cmd.Wait(); err != nil { - return err - } - return nil + return cmd.Wait() } -func (p *Manager) RemoveAll() error { +// RemoveAllIfInitialized is a safe way to clean a datadirectory before recreating +func (p *Manager) RemoveAllIfInitialized(ctx context.Context) error { initialized, err := p.IsInitialized() if err != nil { return fmt.Errorf("failed to retrieve instance state: %v", err) @@ -1014,42 +1094,61 @@ func (p *Manager) RemoveAll() error { } } if started { - return fmt.Errorf("cannot remove postregsql database. Instance is active") + return errors.New("cannot remove postregsql database. Instance is active") } + + return p.RemoveAll(ctx) +} + +// RemoveAll entirely cleans up the data directory, including any wal directory if that +// exists outside of the data directory. +func (p *Manager) RemoveAll(ctx context.Context) error { + ctx, logger := logging.GetLogComponent(ctx, logging.PgComponent) + if p.walDir != "" { + if err := os.RemoveAll(p.walDir); err != nil { + logger.Fatal().Str("path", p.walDir).AnErr("err", err).Msg("failed to remove tree") + } + } + return os.RemoveAll(p.dataDir) } -func (p *Manager) GetSystemData() (*SystemData, error) { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// GetSystemData will retrieve and return the system data (IDENTIFY_SYSTEM) +func (p *Manager) GetSystemData(ctx context.Context) (*SystemData, error) { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return GetSystemData(ctx, p.replConnParams) } -func (p *Manager) GetTimelinesHistory(timeline uint64) ([]*TimelineHistory, error) { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// GetTimelinesHistory will return a lost of timelinehostory objects +func (p *Manager) GetTimelinesHistory(ctx context.Context, timeline uint64) ([]*TimelineHistory, error) { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return getTimelinesHistory(ctx, timeline, p.replConnParams) } -func (p *Manager) GetConfigFilePGParameters() (common.Parameters, error) { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// GetConfigFilePGParameters will return the config file parameters (pg_file_settings or pg_settings) +func (p *Manager) GetConfigFilePGParameters(ctx context.Context) (common.Parameters, error) { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return getConfigFilePGParameters(ctx, p.localConnParams) } -func (p *Manager) Ping() error { - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) +// Ping triesd to connect to PostgreSQL and returns an error if it can't +func (p *Manager) Ping(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() return ping(ctx, p.localConnParams) } +// OlderWalFile returns the oldest WAL file in the pg_wal location func (p *Manager) OlderWalFile() (string, error) { - maj, _, err := p.PGDataVersion() + version, err := p.PGDataVersion() if err != nil { return "", err } var walDir string - if maj < 10 { + if version.LessThan(V10) { walDir = "pg_xlog" } else { walDir = "pg_wal" @@ -1060,7 +1159,7 @@ func (p *Manager) OlderWalFile() (string, error) { return "", err } names, err := f.Readdirnames(-1) - f.Close() + handledFileClose(f) if err != nil { return "", err } @@ -1074,7 +1173,7 @@ func (p *Manager) OlderWalFile() (string, error) { } // if the file size is different from the currently supported one // (16Mib) return without checking other possible wal files - if fi.Size() != WalSegSize { + if fi.Size() != walSegSize { return "", fmt.Errorf("wal file has unsupported size: %d", fi.Size()) } return name, nil @@ -1085,18 +1184,34 @@ func (p *Manager) OlderWalFile() (string, error) { } // IsRestartRequired returns if a postgres restart is necessary -func (p *Manager) IsRestartRequired(changedParams []string) (bool, error) { - maj, min, err := p.BinaryVersion() +func (p *Manager) IsRestartRequired(ctx context.Context, changedParams []string) (bool, error) { + version, err := p.BinaryVersion() if err != nil { return false, fmt.Errorf("error fetching pg version: %v", err) } - ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout) + ctx, cancel := context.WithTimeout(ctx, p.requestTimeout) defer cancel() - if maj == 9 && min < 5 { + if version.LessThan(V95) { return isRestartRequiredUsingPgSettingsContext(ctx, p.localConnParams, changedParams) - } else { - return isRestartRequiredUsingPendingRestart(ctx, p.localConnParams) } + return isRestartRequiredUsingPendingRestart(ctx, p.localConnParams) +} + +// GetSystemID is function that fetches the systemID and returns it as a string +func (p *Manager) GetSystemID() (string, error) { + pgControlFile := filepath.Join(p.dataDir, "global", "pg_control") + pgControl, err := os.Open(pgControlFile) + if err != nil { + return "", err + } + defer handledFileClose(pgControl) + var systemID uint64 + err = binary.Read(pgControl, binary.NativeEndian, &systemID) + if err != nil { + return "", err + } + const baseTen = 10 + return strconv.FormatUint(systemID, baseTen), nil } diff --git a/internal/postgresql/control.go b/internal/postgresql/pgmanager.go similarity index 57% rename from internal/postgresql/control.go rename to internal/postgresql/pgmanager.go index 84a25b6e8..2639af8a0 100644 --- a/internal/postgresql/control.go +++ b/internal/postgresql/pgmanager.go @@ -1,4 +1,5 @@ -// Copyright 2016 Sorint.lab +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,22 +15,11 @@ package postgresql -import ( - "encoding/binary" - "os" - "path/filepath" - "strconv" -) +import "context" -func (p *Manager) GetSystemdID() (string, error) { - pgControl, err := os.Open(filepath.Join(p.dataDir, "global", "pg_control")) - if err != nil { - return "", err - } - var systemID uint64 - err = binary.Read(pgControl, binary.LittleEndian, &systemID) - if err != nil { - return "", err - } - return strconv.FormatUint(systemID, 10), nil +//go:generate mockgen -destination=../mock/postgresql/postgresql.go -package=mocks -source=$GOFILE + +// PGManager can retrieve the timeline history of a PostgreSQL instance +type PGManager interface { + GetTimelinesHistory(ctx context.Context, timeline uint64) ([]*TimelineHistory, error) } diff --git a/internal/postgresql/pgversion_test.go b/internal/postgresql/pgversion_test.go new file mode 100644 index 000000000..5790eb72e --- /dev/null +++ b/internal/postgresql/pgversion_test.go @@ -0,0 +1,174 @@ +// Copyright 2025 Nibble-IT +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +import ( + "fmt" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Pgversion", func() { + When("Parsing version from a binary", func() { + It("should successfully parse", func() { + for _, test := range []struct { + in string + expected string + }{ + {in: "v10", expected: "10.0.0"}, + {in: "10", expected: "10.0.0"}, + {in: "9.5.7", expected: "9.5.7"}, + {in: "9.5.7-rc1", expected: "9.5.7-rc1"}, + {in: "11-beta1", expected: "11.0.0-beta1"}, + } { + fmt.Fprintf(GinkgoWriter, "DEBUG - Test: %v\n", test) + version, err := parseVersion(test.in) + Ω(err).NotTo(HaveOccurred()) + Ω(version.String()).To(Equal(test.expected)) + } + }) + It("should fail on", func() { + for _, test := range []struct { + in string + }{ + {in: "v"}, + {in: "1.2.3.4"}, + } { + fmt.Fprintf(GinkgoWriter, "DEBUG - Test: %v\n", test) + version, err := parseVersion(test.in) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + } + }) + }) + When("Comparing", func() { + It("should properly check greater than or equal", func() { + for _, test := range []struct { + in string + ge string + }{ + {in: "10", ge: "10"}, + {in: "10", ge: "9.6"}, + {in: "9.6", ge: "9.5.7"}, + {in: "9.5.7", ge: "9.5.7-rc1"}, + {in: "9.5.7-rc1", ge: "9.5.7-beta1"}, + {in: "9.6-beta1", ge: "9.5.7-rc1"}, + } { + fmt.Fprintf(GinkgoWriter, "DEBUG - Test: %v\n", test) + inVersion, err := parseVersion(test.in) + Ω(err).NotTo(HaveOccurred()) + geVersion, err := parseVersion(test.ge) + Ω(err).NotTo(HaveOccurred()) + Ω(inVersion.GreaterThanEqual(geVersion)).To(BeTrue()) + } + }) + }) + When("Getting version from PGDATA", func() { + It("should successfully work for a proper DATADIR", func() { + tempDir, err := os.MkdirTemp("", "pgdvs") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + dataDir := filepath.Join(tempDir, "proper_dir") + err = os.Mkdir(dataDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + pgVersionFile := filepath.Join(dataDir, "PG_VERSION") + err = os.WriteFile(pgVersionFile, []byte("18"), urw) + version, err := pgDataVersion(dataDir) + Ω(err).NotTo(HaveOccurred()) + Ω(version.Equal(V18)).To(BeTrue()) + }) + It("should fail when DATADIR does not exist", func() { + tempDir, err := os.MkdirTemp("", "pgdvd") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + dataDir := filepath.Join(tempDir, "proper_dir") + version, err := pgDataVersion(dataDir) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + }) + It("should fail when PGVERSION does not exist", func() { + tempDir, err := os.MkdirTemp("", "pgdvv") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + dataDir := filepath.Join(tempDir, "proper_dir") + err = os.Mkdir(dataDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + version, err := pgDataVersion(dataDir) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + }) + It("should fail when PGVERSION contains nonsense", func() { + tempDir, err := os.MkdirTemp("", "pgdvs") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + dataDir := filepath.Join(tempDir, "proper_dir") + err = os.Mkdir(dataDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + pgVersionFile := filepath.Join(dataDir, "PG_VERSION") + err = os.WriteFile(pgVersionFile, []byte("aabbccd"), urw) + version, err := pgDataVersion(dataDir) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + }) + }) + When("Getting version from a binary", func() { + It("should successfully work for a proper postgres binary", func() { + tempDir, err := os.MkdirTemp("", "pgbvs") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + binDir := filepath.Join(tempDir, "bindir") + err = os.Mkdir(binDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + pgBinary := filepath.Join(binDir, "postgres") + err = os.WriteFile(pgBinary, []byte( + `#!/bin/bash +echo "postgres (PostgreSQL) 18.0 (Debian 18.0-1.pgdg13+3)"`, + ), urwx) + version, err := binaryVersion(binDir) + Ω(err).NotTo(HaveOccurred()) + Ω(version.Equal(V18)).To(BeTrue()) + }) + It("should fail when binary does not exist", func() { + tempDir, err := os.MkdirTemp("", "pgbvb") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + binDir := filepath.Join(tempDir, "bindir") + err = os.Mkdir(binDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + version, err := binaryVersion(binDir) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + }) + It("should fail when binary returns nonsense", func() { + tempDir, err := os.MkdirTemp("", "pgbvb") + Ω(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tempDir) // Clean up after test + binDir := filepath.Join(tempDir, "bindir") + err = os.Mkdir(binDir, urwx) // revive:disable-line:add-constant + Ω(err).NotTo(HaveOccurred()) + pgBinary := filepath.Join(binDir, "postgres") + err = os.WriteFile(pgBinary, []byte( + `#!/bin/bash +echo "this is no good"`, + ), urwx) + version, err := binaryVersion(binDir) + Ω(err).To(HaveOccurred()) + Ω(version).To(BeNil()) + }) + }) +}) diff --git a/internal/postgresql/postgresql_suite_test.go b/internal/postgresql/postgresql_suite_test.go new file mode 100644 index 000000000..e4b2c0a9e --- /dev/null +++ b/internal/postgresql/postgresql_suite_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 Nibble-IT +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. +package postgresql + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPostgresql(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Postgresql Suite") +} diff --git a/internal/postgresql/recovery_options.go b/internal/postgresql/recovery_options.go new file mode 100644 index 000000000..63f0101ab --- /dev/null +++ b/internal/postgresql/recovery_options.go @@ -0,0 +1,60 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +import ( + "reflect" + + "github.com/pgvillage-tools/stolon/internal/common" + + "github.com/mitchellh/copystructure" +) + +// RecoveryMode is an enum for the type of recover an instance is in +type RecoveryMode int + +const ( + // RecoveryModeNone is set when an instance is a primary instance + RecoveryModeNone RecoveryMode = iota + // RecoveryModeStandby is set when an instance is a replica instance + RecoveryModeStandby + // RecoveryModeRecovery is set during Point in time recovery + RecoveryModeRecovery +) + +// RecoveryOptions set recovery options +type RecoveryOptions struct { + RecoveryMode RecoveryMode + RecoveryParameters common.Parameters +} + +// NewRecoveryOptions returns a freshly initialized Recoveryoptions resource +func NewRecoveryOptions() *RecoveryOptions { + return &RecoveryOptions{RecoveryParameters: make(common.Parameters)} +} + +// DeepCopy returns a full copy +func (r *RecoveryOptions) DeepCopy() (ro *RecoveryOptions) { + var ok bool + if nr, err := copystructure.Copy(r); err != nil { + panic(err) + } else if !reflect.DeepEqual(r, nr) { + panic("not equal") + } else if ro, ok = nr.(*RecoveryOptions); !ok { + panic("type is different after copy") + } + return ro +} diff --git a/internal/postgresql/system_data.go b/internal/postgresql/system_data.go new file mode 100644 index 000000000..707438e94 --- /dev/null +++ b/internal/postgresql/system_data.go @@ -0,0 +1,23 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +// SystemData stores the system specific data of a PostgreSQL instance +type SystemData struct { + SystemID string + TimelineID uint64 + XLogPos uint64 +} diff --git a/internal/postgresql/timeline_history.go b/internal/postgresql/timeline_history.go new file mode 100644 index 000000000..d65b0f652 --- /dev/null +++ b/internal/postgresql/timeline_history.go @@ -0,0 +1,23 @@ +// Copyright 2026 PgVillage +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgresql + +// TimelineHistory stores all info regarding a specific timeline +type TimelineHistory struct { + TimelineID uint64 + SwitchPoint uint64 + Reason string +} diff --git a/internal/postgresql/utils.go b/internal/postgresql/utils.go deleted file mode 100644 index 4ca50cd59..000000000 --- a/internal/postgresql/utils.go +++ /dev/null @@ -1,565 +0,0 @@ -// Copyright 2015 Sorint.lab -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied -// See the License for the specific language governing permissions and -// limitations under the License. - -package postgresql - -import ( - "bufio" - "context" - "database/sql" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/sorintlab/stolon/internal/common" - - "os" - - "github.com/lib/pq" -) - -const ( - // TODO(sgotti) for now we assume wal size is the default 16MiB size - WalSegSize = (16 * 1024 * 1024) // 16MiB -) - -var ( - ValidReplSlotName = regexp.MustCompile("^[a-z0-9_]+$") -) - -func dbExec(ctx context.Context, db *sql.DB, query string, args ...interface{}) (sql.Result, error) { - return db.ExecContext(ctx, query, args...) -} - -func query(ctx context.Context, db *sql.DB, query string, args ...interface{}) (*sql.Rows, error) { - return db.QueryContext(ctx, query, args...) -} - -func ping(ctx context.Context, connParams ConnParams) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - _, err = dbExec(ctx, db, "select 1") - if err != nil { - return err - } - return nil -} - -func setPassword(ctx context.Context, connParams ConnParams, username, password string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - tx, err := db.Begin() - if err != nil { - return err - } - - query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - - query = fmt.Sprintf("alter role %s with encrypted password %s", pq.QuoteIdentifier(username), pq.QuoteLiteral(password)) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - return tx.Commit() -} - -func createRole(ctx context.Context, connParams ConnParams, roles []string, username, password string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - tx, err := db.Begin() - if err != nil { - return err - } - - query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - - query = fmt.Sprintf("create role %s with login replication encrypted password %s", pq.QuoteIdentifier(username), pq.QuoteLiteral(password)) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - return tx.Commit() -} - -func createPasswordlessRole(ctx context.Context, connParams ConnParams, roles []string, username string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - _, err = dbExec(ctx, db, fmt.Sprintf(`create role "%s" with login replication;`, username)) - return err -} - -func alterRole(ctx context.Context, connParams ConnParams, roles []string, username, password string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - tx, err := db.Begin() - if err != nil { - return err - } - - query := fmt.Sprintf("set local log_statement = %s", pq.QuoteLiteral("none")) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - - query = fmt.Sprintf("alter role %s with login replication encrypted password %s", pq.QuoteIdentifier(username), pq.QuoteLiteral(password)) - if _, err = tx.ExecContext(ctx, query); err != nil { - _ = tx.Rollback() - return err - } - return tx.Commit() -} - -func alterPasswordlessRole(ctx context.Context, connParams ConnParams, roles []string, username string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - _, err = dbExec(ctx, db, fmt.Sprintf(`alter role "%s" with login replication;`, username)) - return err -} - -// getReplicatinSlots return existing replication slots. On PostgreSQL > 10 we -// skip temporary slots. -func getReplicationSlots(ctx context.Context, connParams ConnParams, maj int) ([]string, error) { - var q string - if maj < 10 { - q = "select slot_name from pg_replication_slots" - } else { - q = "select slot_name from pg_replication_slots where temporary is false" - } - - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return nil, err - } - defer db.Close() - - replSlots := []string{} - - rows, err := query(ctx, db, q) - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var slotName string - if err := rows.Scan(&slotName); err != nil { - return nil, err - } - replSlots = append(replSlots, slotName) - } - - return replSlots, nil -} - -func createReplicationSlot(ctx context.Context, connParams ConnParams, name string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - _, err = dbExec(ctx, db, fmt.Sprintf("select pg_create_physical_replication_slot('%s')", name)) - return err -} - -func dropReplicationSlot(ctx context.Context, connParams ConnParams, name string) error { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return err - } - defer db.Close() - - _, err = dbExec(ctx, db, fmt.Sprintf("select pg_drop_replication_slot('%s')", name)) - return err -} - -func getSyncStandbys(ctx context.Context, connParams ConnParams) ([]string, error) { - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return nil, err - } - defer db.Close() - - rows, err := query(ctx, db, "select application_name, sync_state from pg_stat_replication") - if err != nil { - return nil, err - } - defer rows.Close() - - syncStandbys := []string{} - for rows.Next() { - var applicationName, syncState string - if err := rows.Scan(&applicationName, &syncState); err != nil { - return nil, err - } - - if syncState == "sync" { - syncStandbys = append(syncStandbys, applicationName) - } - } - - return syncStandbys, nil -} - -func PGLsnToInt(lsn string) (uint64, error) { - parts := strings.Split(lsn, "/") - if len(parts) != 2 { - return 0, fmt.Errorf("bad pg_lsn: %s", lsn) - } - a, err := strconv.ParseUint(parts[0], 16, 32) - if err != nil { - return 0, err - } - b, err := strconv.ParseUint(parts[1], 16, 32) - if err != nil { - return 0, err - } - v := uint64(a)<<32 | b - return v, nil -} - -func GetSystemData(ctx context.Context, replConnParams ConnParams) (*SystemData, error) { - // Add "replication=1" connection option - replConnParams["replication"] = "1" - db, err := sql.Open("postgres", replConnParams.ConnString()) - if err != nil { - return nil, err - } - defer db.Close() - - rows, err := query(ctx, db, "IDENTIFY_SYSTEM") - if err != nil { - return nil, err - } - defer rows.Close() - if rows.Next() { - var sd SystemData - var xLogPosLsn string - var unused *string - if err = rows.Scan(&sd.SystemID, &sd.TimelineID, &xLogPosLsn, &unused); err != nil { - return nil, err - } - sd.XLogPos, err = PGLsnToInt(xLogPosLsn) - if err != nil { - return nil, err - } - return &sd, nil - } - return nil, fmt.Errorf("query returned 0 rows") -} - -func parseTimelinesHistory(contents string) ([]*TimelineHistory, error) { - tlsh := []*TimelineHistory{} - regex, err := regexp.Compile(`(\S+)\s+(\S+)\s+(.*)$`) - if err != nil { - return nil, err - } - - scanner := bufio.NewScanner(strings.NewReader(contents)) - scanner.Split(bufio.ScanLines) - - for scanner.Scan() { - m := regex.FindStringSubmatch(scanner.Text()) - if len(m) == 4 { - var tlh TimelineHistory - if tlh.TimelineID, err = strconv.ParseUint(m[1], 10, 64); err != nil { - return nil, fmt.Errorf("cannot parse timelineID in timeline history line %q: %v", scanner.Text(), err) - } - if tlh.SwitchPoint, err = PGLsnToInt(m[2]); err != nil { - return nil, fmt.Errorf("cannot parse start lsn in timeline history line %q: %v", scanner.Text(), err) - } - tlh.Reason = m[3] - tlsh = append(tlsh, &tlh) - } - } - return tlsh, err -} - -func getTimelinesHistory(ctx context.Context, timeline uint64, replConnParams ConnParams) ([]*TimelineHistory, error) { - // Add "replication=1" connection option - replConnParams["replication"] = "1" - db, err := sql.Open("postgres", replConnParams.ConnString()) - if err != nil { - return nil, err - } - defer db.Close() - - rows, err := query(ctx, db, fmt.Sprintf("TIMELINE_HISTORY %d", timeline)) - if err != nil { - return nil, err - } - defer rows.Close() - if rows.Next() { - var timelineFile string - var contents string - if err := rows.Scan(&timelineFile, &contents); err != nil { - return nil, err - } - tlsh, err := parseTimelinesHistory(contents) - if err != nil { - return nil, err - } - return tlsh, nil - } - return nil, fmt.Errorf("query returned 0 rows") -} - -func IsValidReplSlotName(name string) bool { - return ValidReplSlotName.MatchString(name) -} - -func fileExists(path string) (bool, error) { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} - -func expand(s, dataDir string) string { - buf := make([]byte, 0, 2*len(s)) - // %d %% are all ASCII, so bytes are fine for this operation. - i := 0 - for j := 0; j < len(s); j++ { - if s[j] == '%' && j+1 < len(s) { - switch s[j+1] { - case 'd': - buf = append(buf, s[i:j]...) - buf = append(buf, []byte(dataDir)...) - j += 1 - i = j + 1 - case '%': - j += 1 - buf = append(buf, s[i:j]...) - i = j + 1 - default: - } - } - } - return string(buf) + s[i:] -} - -func getConfigFilePGParameters(ctx context.Context, connParams ConnParams) (common.Parameters, error) { - var pgParameters = common.Parameters{} - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return nil, err - } - defer db.Close() - - // We prefer pg_file_settings since pg_settings returns archive_command = '(disabled)' when archive_mode is off so we'll lose its value - // Check if pg_file_settings exists (pg >= 9.5) - rows, err := query(ctx, db, "select 1 from information_schema.tables where table_schema = 'pg_catalog' and table_name = 'pg_file_settings'") - if err != nil { - return nil, err - } - defer rows.Close() - c := 0 - for rows.Next() { - c++ - } - use_pg_file_settings := false - if c > 0 { - use_pg_file_settings = true - } - - if use_pg_file_settings { - // NOTE If some pg_parameters that cannot be changed without a restart - // are removed from the postgresql.conf file the view will contain some - // rows with null name and setting and the error field set to the cause. - // So we have to filter out these or the Scan will fail. - rows, err = query(ctx, db, "select name, setting from pg_file_settings where name IS NOT NULL and setting IS NOT NULL") - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var name, setting string - if err = rows.Scan(&name, &setting); err != nil { - return nil, err - } - pgParameters[name] = setting - } - return pgParameters, nil - } - - // Fallback to pg_settings - rows, err = query(ctx, db, "select name, setting, source from pg_settings") - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var name, setting, source string - if err = rows.Scan(&name, &setting, &source); err != nil { - return nil, err - } - if source == "configuration file" { - pgParameters[name] = setting - } - } - return pgParameters, nil -} - -func isRestartRequiredUsingPendingRestart(ctx context.Context, connParams ConnParams) (bool, error) { - isRestartRequired := false - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return isRestartRequired, err - } - defer db.Close() - - rows, err := query(ctx, db, "select count(*) > 0 from pg_settings where pending_restart;") - if err != nil { - return isRestartRequired, err - } - defer rows.Close() - if rows.Next() { - if err := rows.Scan(&isRestartRequired); err != nil { - return isRestartRequired, err - } - } - - return isRestartRequired, nil -} - -func isRestartRequiredUsingPgSettingsContext(ctx context.Context, connParams ConnParams, changedParams []string) (bool, error) { - isRestartRequired := false - db, err := sql.Open("postgres", connParams.ConnString()) - if err != nil { - return isRestartRequired, err - } - defer db.Close() - - stmt, err := db.Prepare("select count(*) > 0 from pg_settings where context = 'postmaster' and name = ANY($1)") - - if err != nil { - return false, err - } - - rows, err := stmt.Query(pq.Array(changedParams)) - if err != nil { - return isRestartRequired, err - } - defer rows.Close() - if rows.Next() { - if err := rows.Scan(&isRestartRequired); err != nil { - return isRestartRequired, err - } - } - - return isRestartRequired, nil -} - -func ParseBinaryVersion(v string) (int, int, error) { - // extract version (removing beta*, rc* etc...) - regex, err := regexp.Compile(`.* \(PostgreSQL\) ([0-9\.]+).*`) - if err != nil { - return 0, 0, err - } - m := regex.FindStringSubmatch(v) - if len(m) != 2 { - return 0, 0, fmt.Errorf("failed to parse postgres binary version: %q", v) - } - return ParseVersion(m[1]) -} - -func ParseVersion(v string) (int, int, error) { - parts := strings.Split(v, ".") - if len(parts) < 1 { - return 0, 0, fmt.Errorf("bad version: %q", v) - } - maj, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse major %q: %v", parts[0], err) - } - min := 0 - if len(parts) > 1 { - min, err = strconv.Atoi(parts[1]) - if err != nil { - return 0, 0, fmt.Errorf("failed to parse minor %q: %v", parts[1], err) - } - } - - return maj, min, nil -} - -func IsWalFileName(name string) bool { - walChars := "0123456789ABCDEF" - if len(name) != 24 { - return false - } - for _, c := range name { - ok := false - for _, v := range walChars { - if c == v { - ok = true - } - } - if !ok { - return false - } - } - return true -} - -func XlogPosToWalFileNameNoTimeline(XLogPos uint64) string { - id := uint32(XLogPos >> 32) - offset := uint32(XLogPos) - // TODO(sgotti) for now we assume wal size is the default 16M size - seg := offset / WalSegSize - return fmt.Sprintf("%08X%08X", id, seg) -} - -func WalFileNameNoTimeLine(name string) (string, error) { - if !IsWalFileName(name) { - return "", fmt.Errorf("bad wal file name") - } - return name[8:24], nil -} diff --git a/internal/postgresql/utils_test.go b/internal/postgresql/utils_test.go index 0f7eedb31..04226caa0 100644 --- a/internal/postgresql/utils_test.go +++ b/internal/postgresql/utils_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +23,7 @@ import ( ) func TestParseTimelineHistory(t *testing.T) { + const someHugeSwitchPoint = 83886224 tests := []struct { contents string tlsh []*TimelineHistory @@ -37,7 +39,7 @@ func TestParseTimelineHistory(t *testing.T) { tlsh: []*TimelineHistory{ { TimelineID: 1, - SwitchPoint: 83886224, + SwitchPoint: someHugeSwitchPoint, Reason: "no recovery target specified", }, }, @@ -59,11 +61,10 @@ func TestParseTimelineHistory(t *testing.T) { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(tlsh, tt.tlsh) { - t.Errorf(spew.Sprintf("#%d: wrong timeline history: got: %#+v, want: %#+v", i, tlsh, tt.tlsh)) + t.Error(spew.Sprintf("#%d: wrong timeline history: got: %#+v, want: %#+v", i, tlsh, tt.tlsh)) } } } - } func TestValidReplSlotName(t *testing.T) { @@ -89,7 +90,7 @@ func TestValidReplSlotName(t *testing.T) { } } -func TestExpand(t *testing.T) { +func TestExpandRecoveryCommand(t *testing.T) { tests := []struct { in string out string @@ -106,6 +107,10 @@ func TestExpand(t *testing.T) { in: "%d", out: "/datadir", }, + { + in: "%w", + out: "/waldir", + }, { in: "%%d", out: "%d", @@ -121,96 +126,13 @@ func TestExpand(t *testing.T) { } for i, tt := range tests { - out := expand(tt.in, "/datadir") + out := expandRecoveryCommand(tt.in, "/datadir", "/waldir") if out != tt.out { t.Errorf("#%d: wrong expanded string: got: %s, want: %s", i, out, tt.out) } } } -func TestParseBinaryVersion(t *testing.T) { - tests := []struct { - in string - maj int - min int - err error - }{ - { - in: "postgres (PostgreSQL) 9.5.7", - maj: 9, - min: 5, - }, - { - in: "postgres (PostgreSQL) 9.6.7\n", - maj: 9, - min: 6, - }, - { - in: "postgres (PostgreSQL) 10beta1", - maj: 10, - min: 0, - }, - { - in: "postgres (PostgreSQL) 10.1.2", - maj: 10, - min: 1, - }, - } - - for i, tt := range tests { - maj, min, err := ParseBinaryVersion(tt.in) - t.Logf("test #%d", i) - if tt.err != nil { - if err == nil { - t.Fatalf("got no error, wanted error: %v", tt.err) - } else if tt.err.Error() != err.Error() { - t.Fatalf("got error: %v, wanted error: %v", err, tt.err) - } - } else { - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if maj != tt.maj || min != tt.min { - t.Fatalf("#%d: wrong maj.min version: got: %d.%d, want: %d.%d", i, maj, min, tt.maj, tt.min) - } - } - } -} - -func TestParseVersion(t *testing.T) { - tests := []struct { - in string - maj int - min int - err error - }{ - { - in: "9.5.7", - maj: 9, - min: 5, - }, - } - - for i, tt := range tests { - maj, min, err := ParseVersion(tt.in) - t.Logf("test #%d", i) - if tt.err != nil { - if err == nil { - t.Fatalf("got no error, wanted error: %v", tt.err) - } else if tt.err.Error() != err.Error() { - t.Fatalf("got error: %v, wanted error: %v", err, tt.err) - } - } else { - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if maj != tt.maj || min != tt.min { - t.Fatalf("#%d: wrong maj.min versio : got: %d.%d, want: %d.%d", i, maj, min, tt.maj, tt.min) - } - } - } -} - func TestIsWalFileName(t *testing.T) { tests := []struct { name string diff --git a/internal/store/election.go b/internal/store/election.go new file mode 100644 index 000000000..44b1f99ad --- /dev/null +++ b/internal/store/election.go @@ -0,0 +1,187 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import ( + "context" + "sync" + "time" + + "github.com/kvtools/valkeyrie/store" +) + +const ( + defaultLockTTL = 20 * time.Second +) + +// Candidate runs the leader election algorithm asynchronously +type Candidate struct { + client store.Store + key string + node string + + electedCh chan bool + lock sync.Mutex + lockTTL time.Duration + leader bool + stopCh chan struct{} + stopRenew chan struct{} + resignCh chan bool + errCh chan error +} + +// NewCandidate creates a new Candidate +func NewCandidate(client store.Store, key, node string, ttl time.Duration) *Candidate { + return &Candidate{ + client: client, + key: key, + node: node, + + leader: false, + lockTTL: ttl, + resignCh: make(chan bool), + stopCh: make(chan struct{}), + } +} + +// IsLeader returns true if the candidate is currently a leader. +func (c *Candidate) IsLeader() bool { + c.lock.Lock() + defer c.lock.Unlock() + return c.leader +} + +// RunForElection starts the leader election algorithm. Updates in status are +// pushed through the ElectedCh channel. +// +// ElectedCh is used to get a channel which delivers signals on +// acquiring or losing leadership. It sends true if we become +// the leader, and false if we lose it. +func (c *Candidate) RunForElection(ctx context.Context) (<-chan bool, <-chan error) { + c.electedCh = make(chan bool) + c.errCh = make(chan error) + + go c.campaign(ctx) + + return c.electedCh, c.errCh +} + +// Stop running for election. +func (c *Candidate) Stop() { + close(c.stopCh) +} + +// Resign forces the candidate to step-down and try again. +// If the candidate is not a leader, it doesn't have any effect. +// Candidate will retry immediately to acquire the leadership. If no-one else +// took it, then the Candidate will end up being a leader again. +func (c *Candidate) Resign() { + c.lock.Lock() + leader := c.leader + c.lock.Unlock() + if !leader { + return + } + select { + case <-c.stopCh: + return + case c.resignCh <- true: + default: + } +} + +func (c *Candidate) update(status bool) { + c.lock.Lock() + + c.leader = status + c.lock.Unlock() + c.electedCh <- status +} + +func (c *Candidate) initLock(ctx context.Context) (store.Locker, error) { + // Give up on the lock session if + // we recovered from a store failure + if c.stopRenew != nil { + close(c.stopRenew) + } + + lockOpts := &store.LockOptions{ + Value: []byte(c.node), + } + + if c.lockTTL > 0 { + lockOpts.TTL = c.lockTTL + } else { + lockOpts.TTL = defaultLockTTL + } + + lockOpts.RenewLock = make(chan struct{}) + c.stopRenew = lockOpts.RenewLock + + lock, err := c.client.NewLock(ctx, c.key, lockOpts) + return lock, err +} + +func (c *Candidate) campaign(ctx context.Context) { + defer close(c.electedCh) + defer close(c.errCh) + + for { + // Start as a follower. + c.update(false) + + lock, err := c.initLock(ctx) + if err != nil { + c.errCh <- err + return + } + + lostCh, err := lock.Lock(ctx) + if err != nil { + c.errCh <- err + return + } + + // Hooray! We acquired the lock therefore we are the new leader. + c.update(true) + + select { + case <-c.resignCh: + // We were asked to resign, give up the lock and go back + // campaigning. + // TODO: implement zerolog + _ = lock.Unlock(ctx) + case <-c.stopCh: + // Give up the leadership and quit. + if c.leader { + // TODO: implement zerolog + _ = lock.Unlock(ctx) + } + return + case <-ctx.Done(): + c.lock.Lock() + wasLeader := c.leader + c.leader = false + c.lock.Unlock() + if wasLeader { + _ = lock.Unlock(ctx) + } + return + case <-lostCh: + // We lost the lock. Someone else is the leader, try again. + } + } +} diff --git a/internal/store/etcdv3.go b/internal/store/etcdv3.go index c18564334..d41bdf956 100644 --- a/internal/store/etcdv3.go +++ b/internal/store/etcdv3.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,15 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package store contains all code regarding consensus stores package store import ( "context" + "log" "time" - etcdclientv3 "go.etcd.io/etcd/clientv3" - "go.etcd.io/etcd/clientv3/concurrency" - "go.etcd.io/etcd/etcdserver/api/v3rpc/rpctypes" + "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + etcdclientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/concurrency" ) func fromEtcV3Error(err error) error { @@ -85,7 +88,13 @@ func (s *etcdV3Store) List(pctx context.Context, directory string) ([]*KVPair, e return kvPairs, nil } -func (s *etcdV3Store) AtomicPut(pctx context.Context, key string, value []byte, previous *KVPair, options *WriteOptions) (*KVPair, error) { +func (s *etcdV3Store) AtomicPut( + pctx context.Context, + key string, + value []byte, + previous *KVPair, + options *WriteOptions, +) (*KVPair, error) { etcdv3Options := []etcdclientv3.OpOption{} if options != nil { if options.TTL > 0 { @@ -148,7 +157,7 @@ type etcdv3Election struct { cancel context.CancelFunc } -func (e *etcdv3Election) RunForElection() (<-chan bool, <-chan error) { +func (e *etcdv3Election) RunForElection(_ context.Context) (<-chan bool, <-chan error) { if e.running { panic("already running") } @@ -176,7 +185,11 @@ func (e *etcdv3Election) Leader() (string, error) { if err != nil { return "", fromEtcV3Error(err) } - defer s.Close() + defer func() { + if err := s.Close(); err != nil { + log.Fatalf("Failed to close session: %v", err) + } + }() etcdElection := concurrency.NewElection(s, e.path) diff --git a/internal/store/k8s.go b/internal/store/k8s.go index a0d74aafd..bb6cca0cc 100644 --- a/internal/store/k8s.go +++ b/internal/store/k8s.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,17 +15,19 @@ package store +// TODO: implement context + import ( "context" "encoding/json" "fmt" "time" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/util" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/util" jsonpatch "github.com/evanphx/json-patch" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -38,16 +41,22 @@ import ( "k8s.io/client-go/util/retry" ) +// ComponentLabelValue is an enum that can be used to set the type of pod type ComponentLabelValue string const ( + // DefaultComponentLabel is the defaultlabel to set when no label is defined DefaultComponentLabel = "component" - KeeperLabelValue ComponentLabelValue = "stolon-keeper" + // KeeperLabelValue states the pod to be a Keeper pod + KeeperLabelValue ComponentLabelValue = "stolon-keeper" + // SentinelLabelValue states the pod to be a sentinel pod SentinelLabelValue ComponentLabelValue = "stolon-sentinel" - ProxyLabelValue ComponentLabelValue = "stolon-proxy" + // ProxyLabelValue states the pod to be a Proxy pod + ProxyLabelValue ComponentLabelValue = "stolon-proxy" ) +// KubeStore is a struct stores information about the pod, for example the client, name and clustername type KubeStore struct { client *kubernetes.Clientset podName string @@ -56,6 +65,7 @@ type KubeStore struct { resourceName string } +// NewKubeStore return a freshly initialized store in k8s func NewKubeStore(kubecli *kubernetes.Clientset, podName, namespace, clusterName string) (*KubeStore, error) { return &KubeStore{ client: kubecli, @@ -76,8 +86,9 @@ func (s *KubeStore) labelSelector(componentLabel ComponentLabelValue) labels.Sel func (s *KubeStore) patchKubeStatusAnnotation(annotationData []byte) error { podsClient := s.client.CoreV1().Pods(s.namespace) + ctx := context.Background() retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - pod, err := podsClient.Get(s.podName, metav1.GetOptions{}) + pod, err := podsClient.Get(ctx, s.podName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get latest version of pod: %v", err) } @@ -101,8 +112,7 @@ func (s *KubeStore) patchKubeStatusAnnotation(annotationData []byte) error { if err != nil { return fmt.Errorf("failed to create pod merge patch: %v", err) } - - _, err = podsClient.Patch(s.podName, types.MergePatchType, patchBytes) + _, err = podsClient.Patch(ctx, s.podName, types.MergePatchType, patchBytes, metav1.PatchOptions{}) return err }) if retryErr != nil { @@ -111,7 +121,8 @@ func (s *KubeStore) patchKubeStatusAnnotation(annotationData []byte) error { return nil } -func (s *KubeStore) AtomicPutClusterData(ctx context.Context, cd *cluster.ClusterData, previous *KVPair) (*KVPair, error) { +// AtomicPutClusterData is an atomic way to write ClusterData to the configmap +func (s *KubeStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Data, previous *KVPair) (*KVPair, error) { cdj, err := json.Marshal(cd) if err != nil { return nil, err @@ -119,7 +130,7 @@ func (s *KubeStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Cluste epsClient := s.client.CoreV1().ConfigMaps(s.namespace) retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - result, err := epsClient.Get(s.resourceName, metav1.GetOptions{}) + result, err := epsClient.Get(ctx, s.resourceName, metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("failed to get latest version of configmap: %v", err) } @@ -140,42 +151,39 @@ func (s *KubeStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Cluste if result.Annotations == nil { // empty annotations but previous isn't nil return ErrKeyModified - } else { - curcd, ok := result.Annotations[util.KubeClusterDataAnnotation] - if ok { - // check that the previous cd is the same as the current one in the - // configmap annotation - if string(previous.Value) != string(curcd) { - return ErrKeyModified - } - } else { - // no cd but previous isn't nil - return ErrKeyModified - } + } + curcd, ok := result.Annotations[util.KubeClusterDataAnnotation] + if !ok { + // no cd but previous isn't nil + return ErrKeyModified + } + // check that the previous cd is the same as the current one in the + // configmap annotation + if string(previous.Value) != string(curcd) { + return ErrKeyModified } } if result.Annotations == nil { result.Annotations = map[string]string{} } result.Annotations[util.KubeClusterDataAnnotation] = string(cdj) - _, err = epsClient.Update(result) + _, err = epsClient.Update(ctx, result, metav1.UpdateOptions{}) return err - } else { - // configmap does not exists + } + // configmap does not exists - // previous isn't nil but configmap doesn't exists - if previous != nil { - return ErrKeyModified - } - annotations := map[string]string{util.KubeClusterDataAnnotation: string(cdj)} - _, err = epsClient.Create(&v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: s.resourceName, - Annotations: annotations, - }, - }) - return err + // previous isn't nil but configmap doesn't exists + if previous != nil { + return ErrKeyModified } + annotations := map[string]string{util.KubeClusterDataAnnotation: string(cdj)} + _, err = epsClient.Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.resourceName, + Annotations: annotations, + }, + }, metav1.CreateOptions{}) + return err }) if retryErr != nil { return nil, fmt.Errorf("update failed: %v", retryErr) @@ -183,7 +191,8 @@ func (s *KubeStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Cluste return &KVPair{Value: cdj}, nil } -func (s *KubeStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) error { +// PutClusterData writes ClusterData to the configmap +func (s *KubeStore) PutClusterData(ctx context.Context, cd *cluster.Data) error { cdj, err := json.Marshal(cd) if err != nil { return err @@ -191,7 +200,7 @@ func (s *KubeStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) epsClient := s.client.CoreV1().ConfigMaps(s.namespace) retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - result, err := epsClient.Get(s.resourceName, metav1.GetOptions{}) + result, err := epsClient.Get(ctx, s.resourceName, metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("failed to get latest version of configmap: %v", err) } @@ -201,19 +210,18 @@ func (s *KubeStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) result.Annotations = map[string]string{} } result.Annotations[util.KubeClusterDataAnnotation] = string(cdj) - _, err = epsClient.Update(result) - return err - } else { - // configmap does not exists - annotations := map[string]string{util.KubeClusterDataAnnotation: string(cdj)} - _, err = epsClient.Create(&v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: s.resourceName, - Annotations: annotations, - }, - }) + _, err = epsClient.Update(ctx, result, metav1.UpdateOptions{}) return err } + // configmap does not exists + annotations := map[string]string{util.KubeClusterDataAnnotation: string(cdj)} + _, err = epsClient.Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.resourceName, + Annotations: annotations, + }, + }, metav1.CreateOptions{}) + return err }) if retryErr != nil { return fmt.Errorf("update failed: %v", retryErr) @@ -221,22 +229,22 @@ func (s *KubeStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) return nil } -func (s *KubeStore) GetClusterData(ctx context.Context) (*cluster.ClusterData, *KVPair, error) { +// GetClusterData reads ClusterData from the configmap +func (s *KubeStore) GetClusterData(ctx context.Context) (*cluster.Data, *KVPair, error) { epsClient := s.client.CoreV1().ConfigMaps(s.namespace) - result, err := epsClient.Get(s.resourceName, metav1.GetOptions{}) + result, err := epsClient.Get(ctx, s.resourceName, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { return nil, nil, nil - } else { - return nil, nil, fmt.Errorf("failed to get latest version of configmap: %v", err) } + return nil, nil, fmt.Errorf("failed to get latest version of configmap: %v", err) } cdj, ok := result.Annotations[util.KubeClusterDataAnnotation] if !ok { return nil, nil, nil } - var cd *cluster.ClusterData + var cd *cluster.Data if err := json.Unmarshal([]byte(cdj), &cd); err != nil { return nil, nil, err } @@ -244,7 +252,8 @@ func (s *KubeStore) GetClusterData(ctx context.Context) (*cluster.ClusterData, * return cd, &KVPair{Value: []byte(cdj)}, nil } -func (s *KubeStore) SetKeeperInfo(ctx context.Context, id string, ms *cluster.KeeperInfo, ttl time.Duration) error { +// SetKeeperInfo updates info to annotations on a Keeper pod +func (s *KubeStore) SetKeeperInfo(_ context.Context, _ string, ms *cluster.KeeperInfo, _ time.Duration) error { msj, err := json.Marshal(ms) if err != nil { return err @@ -252,6 +261,7 @@ func (s *KubeStore) SetKeeperInfo(ctx context.Context, id string, ms *cluster.Ke return s.patchKubeStatusAnnotation(msj) } +// GetKeepersInfo reads info from annotations on all Keeper pods func (s *KubeStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, error) { keepers := cluster.KeepersInfo{} @@ -260,7 +270,7 @@ func (s *KubeStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, er listOpts := metav1.ListOptions{ LabelSelector: s.labelSelector(KeeperLabelValue).String(), } - result, err := podsClient.List(listOpts) + result, err := podsClient.List(ctx, listOpts) if err != nil { return nil, fmt.Errorf("failed to get latest version of pod: %v", err) } @@ -279,7 +289,8 @@ func (s *KubeStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, er return keepers, nil } -func (s *KubeStore) SetSentinelInfo(ctx context.Context, si *cluster.SentinelInfo, ttl time.Duration) error { +// SetSentinelInfo updates info on a Sentinel pod +func (s *KubeStore) SetSentinelInfo(_ context.Context, si *cluster.SentinelInfo, _ time.Duration) error { sij, err := json.Marshal(si) if err != nil { return err @@ -287,6 +298,7 @@ func (s *KubeStore) SetSentinelInfo(ctx context.Context, si *cluster.SentinelInf return s.patchKubeStatusAnnotation(sij) } +// GetSentinelsInfo reads info on all Sentinel pods func (s *KubeStore) GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo, error) { ssi := cluster.SentinelsInfo{} @@ -295,7 +307,7 @@ func (s *KubeStore) GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo listOpts := metav1.ListOptions{ LabelSelector: s.labelSelector(SentinelLabelValue).String(), } - result, err := podsClient.List(listOpts) + result, err := podsClient.List(ctx, listOpts) if err != nil { return nil, fmt.Errorf("failed to get latest version of pod: %v", err) } @@ -314,7 +326,8 @@ func (s *KubeStore) GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo return ssi, nil } -func (s *KubeStore) SetProxyInfo(ctx context.Context, pi *cluster.ProxyInfo, ttl time.Duration) error { +// SetProxyInfo updates info to annotations on a Proxy pod +func (s *KubeStore) SetProxyInfo(_ context.Context, pi *cluster.ProxyInfo, _ time.Duration) error { pij, err := json.Marshal(pi) if err != nil { return err @@ -322,6 +335,7 @@ func (s *KubeStore) SetProxyInfo(ctx context.Context, pi *cluster.ProxyInfo, ttl return s.patchKubeStatusAnnotation(pij) } +// GetProxiesInfo reads info from annotations on all Proxy pods func (s *KubeStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, error) { psi := cluster.ProxiesInfo{} @@ -330,7 +344,7 @@ func (s *KubeStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, er listOpts := metav1.ListOptions{ LabelSelector: s.labelSelector(ProxyLabelValue).String(), } - result, err := podsClient.List(listOpts) + result, err := podsClient.List(ctx, listOpts) if err != nil { return nil, fmt.Errorf("failed to get latest version of pod: %v", err) } @@ -349,6 +363,7 @@ func (s *KubeStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, er return psi, nil } +// KubeElection takes care of the election proces for stolon on k8s type KubeElection struct { client *kubernetes.Clientset podName string @@ -366,10 +381,13 @@ type KubeElection struct { rl resourcelock.Interface } -func NewKubeElection(kubecli *kubernetes.Clientset, podName, namespace, clusterName, candidateUID string) (*KubeElection, error) { +// NewKubeElection returns a freshly initialized KubeElection resource +func NewKubeElection( + kubecli *kubernetes.Clientset, + podName, namespace, clusterName, candidateUID string, +) (*KubeElection, error) { resourceName := fmt.Sprintf("%s-%s", util.KubeResourcePrefix, clusterName) - - rl, err := resourcelock.New(resourcelock.ConfigMapsResourceLock, + rl, err := resourcelock.New("configmaps", namespace, resourceName, kubecli.CoreV1(), @@ -378,6 +396,7 @@ func NewKubeElection(kubecli *kubernetes.Clientset, podName, namespace, clusterN Identity: candidateUID, EventRecorder: createRecorder(kubecli, "stolon-sentinel", namespace), }) + // https://github.com/kubernetes/client-go/blob/master/tools/leaderelection/resourcelock/interface.go if err != nil { return nil, fmt.Errorf("error creating lock: %v", err) } @@ -391,14 +410,15 @@ func NewKubeElection(kubecli *kubernetes.Clientset, podName, namespace, clusterN }, nil } -func (e *KubeElection) RunForElection() (<-chan bool, <-chan error) { +// RunForElection starts a campaign for getting elected +func (e *KubeElection) RunForElection(ctx context.Context) (<-chan bool, <-chan error) { if e.running { panic("already running") } e.electedCh = make(chan bool) e.errCh = make(chan error) - e.ctx, e.cancel = context.WithCancel(context.Background()) + e.ctx, e.cancel = context.WithCancel(ctx) e.running = true go e.campaign() @@ -406,6 +426,7 @@ func (e *KubeElection) RunForElection() (<-chan bool, <-chan error) { return e.electedCh, e.errCh } +// Stop cancels campaign func (e *KubeElection) Stop() { if !e.running { panic("not running") @@ -414,8 +435,10 @@ func (e *KubeElection) Stop() { e.running = false } +// Leader returns the current leader func (e *KubeElection) Leader() (string, error) { - ler, _, err := e.rl.Get() + ctx := context.Background() + ler, _, err := e.rl.Get(ctx) if err != nil { return "", fmt.Errorf("failed to get leader election record: %v", err) } @@ -452,6 +475,7 @@ func (e *KubeElection) campaign() { func createRecorder(kubecli kubernetes.Interface, name, namespace string) record.EventRecorder { eventBroadcaster := record.NewBroadcaster() - eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: v1core.New(kubecli.CoreV1().RESTClient()).Events(namespace)}) - return eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: name}) + eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{ + Interface: v1core.New(kubecli.CoreV1().RESTClient()).Events(namespace)}) + return eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: name}) } diff --git a/internal/store/kvbacked.go b/internal/store/kvbacked.go index faa1cc615..c4ff02cca 100644 --- a/internal/store/kvbacked.go +++ b/internal/store/kvbacked.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +19,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "net/url" "path/filepath" @@ -25,23 +27,37 @@ import ( "strings" "time" - "github.com/docker/leadership" - "github.com/docker/libkv" - libkvstore "github.com/docker/libkv/store" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - etcdclientv3 "go.etcd.io/etcd/clientv3" + "github.com/kvtools/consul" + "github.com/kvtools/etcdv2" + "github.com/kvtools/etcdv3" + "github.com/kvtools/valkeyrie" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/logging" ) -// Backend represents a KV Store Backend -type Backend string +// BackendType represents a type of KV Store BackendType +type BackendType string const ( - CONSUL Backend = "consul" - ETCDV2 Backend = "etcdv2" - ETCDV3 Backend = "etcdv3" + // CONSUL means that consul is used as backend + CONSUL BackendType = consul.StoreName + // ETCDV2 means that etcd is used as backend and that the v2 api is used + ETCDV2 BackendType = etcdv2.StoreName + // ETCDV3 means that etcd is used as backend and that the v3 api is used + ETCDV3 BackendType = etcdv3.StoreName ) +var storeTypes = []string{ + consul.StoreName, + etcdv2.StoreName, + etcdv3.StoreName, +} + +func (bt BackendType) string() string { + return string(bt) +} + const ( keepersInfoDir = "/keepers/info/" clusterDataFile = "clusterdata" @@ -50,20 +66,22 @@ const ( ) const ( - DefaultEtcdEndpoints = "http://127.0.0.1:2379" + // DefaultEtcdEndpoints defines the default endpoints when using etcd + DefaultEtcdEndpoints = "http://127.0.0.1:2379" + // DefaultConsulEndpoints defines the default endpoints when using consul DefaultConsulEndpoints = "http://127.0.0.1:8500" ) -const ( - //TODO(sgotti) fix this in libkv? - // consul min ttl is 10s and libkv divides this by 2 - MinTTL = 20 * time.Second -) +// TODO(sgotti) fix this in libkv? +// consul min ttl is 10s and libkv divides this by 2 +const minTTL = 20 * time.Second +const dialTimeout = 20 * time.Second -var URLSchemeRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9+-.]*)://`) +var urlSchemeRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9+-.]*)://`) +// Config defines the config to be used for the endpoint type Config struct { - Backend Backend + Backend BackendType Endpoints string Timeout time.Duration BasePath string @@ -73,6 +91,109 @@ type Config struct { SkipTLSVerify bool } +// TLSConfig creates and returns a TLSConfig from a Config if applicable +func (c Config) TLSConfig() (*tls.Config, error) { + scheme, err := c.EndpointScheme() + if err != nil { + return nil, err + } + if scheme != "http" && scheme != "https" { + return nil, errors.New("endpoints scheme must be http or https") + } + if scheme != "https" { + return nil, nil + } + + tlsConfig, err := common.NewTLSConfig(c.CertFile, c.KeyFile, c.CAFile, c.SkipTLSVerify) + if err != nil { + return nil, fmt.Errorf("cannot create store tls config: %v", err) + } + return tlsConfig, nil +} + +// EndpointScheme returns the scheme of all endpoints +// (they should all have the same scheme, or an error occurs) +func (c Config) EndpointScheme() (string, error) { + endpoints, err := c.EndPoints() + if err != nil { + return "", err + } + var scheme string + for _, e := range endpoints { + var curscheme string + if urlSchemeRegexp.Match([]byte(e)) { + u, err := url.Parse(e) + if err != nil { + return "", fmt.Errorf("cannot parse endpoint %q: %v", e, err) + } + curscheme = u.Scheme + } else { + // Assume it's a schemeless endpoint + curscheme = "http" + } + if scheme == "" { + scheme = curscheme + } + if scheme != curscheme { + return "", errors.New("all the endpoints must have the same scheme") + } + } + return scheme, nil +} + +// EndpointAddrs returns the addresses of the endpoints +func (c Config) EndpointAddrs() ([]string, error) { + endpoints, err := c.EndPoints() + if err != nil { + return nil, err + } + addrs := []string{} + var scheme string + for _, e := range endpoints { + var curscheme, addr string + if urlSchemeRegexp.Match([]byte(e)) { + u, err := url.Parse(e) + if err != nil { + return nil, fmt.Errorf("cannot parse endpoint %q: %v", e, err) + } + curscheme = u.Scheme + addr = u.Host + } else { + // Assume it's a schemeless endpoint + curscheme = "http" + addr = e + } + if scheme == "" { + scheme = curscheme + } + if scheme != curscheme { + return nil, errors.New("all the endpoints must have the same scheme") + } + addrs = append(addrs, addr) + } + return addrs, nil +} + +// EndPoints returns a list of endpoints as strings +func (c Config) EndPoints() ([]string, error) { + endpointsStr := c.Endpoints + if endpointsStr == "" { + switch c.Backend { + case CONSUL: + endpointsStr = DefaultConsulEndpoints + case ETCDV2, ETCDV3: + endpointsStr = DefaultEtcdEndpoints + default: + return nil, fmt.Errorf( + "unexpected store '%s', should be any of %v", + c.Backend, + strings.Join(storeTypes, ","), + ) + } + } + return strings.Split(endpointsStr, ","), nil +} + // KVPair represents {Key, Value, Lastindex} tuple type KVPair struct { Key string @@ -80,10 +201,12 @@ type KVPair struct { LastIndex uint64 } +// WriteOptions defines options to be used when writing to the backend type WriteOptions struct { TTL time.Duration } +// KVStore is an interface representing a backend (e.a. etcd, k8s en Consul) type KVStore interface { // Put a value at the specified key Put(ctx context.Context, key string, value []byte, options *WriteOptions) error @@ -104,107 +227,58 @@ type KVStore interface { Close() error } -func NewKVStore(cfg Config) (KVStore, error) { - var kvBackend libkvstore.Backend - switch cfg.Backend { - case CONSUL: - kvBackend = libkvstore.CONSUL - case ETCDV2: - kvBackend = libkvstore.ETCD - case ETCDV3: - default: - return nil, fmt.Errorf("Unknown store backend: %q", cfg.Backend) - } - - endpointsStr := cfg.Endpoints - if endpointsStr == "" { - switch cfg.Backend { - case CONSUL: - endpointsStr = DefaultConsulEndpoints - case ETCDV2, ETCDV3: - endpointsStr = DefaultEtcdEndpoints - } - } - endpoints := strings.Split(endpointsStr, ",") - - // 1) since libkv wants endpoints as a list of IP and not URLs but we - // want to also support them then parse and strip them - // 2) since libkv will enable TLS for all endpoints when config.TLS - // isn't nil we have to check that all the endpoints have the same - // scheme - addrs := []string{} - var scheme string - for _, e := range endpoints { - var curscheme, addr string - if URLSchemeRegexp.Match([]byte(e)) { - u, err := url.Parse(e) - if err != nil { - return nil, fmt.Errorf("cannot parse endpoint %q: %v", e, err) - } - curscheme = u.Scheme - addr = u.Host - } else { - // Assume it's a schemeless endpoint - curscheme = "http" - addr = e - } - if scheme == "" { - scheme = curscheme - } - if scheme != curscheme { - return nil, fmt.Errorf("all the endpoints must have the same scheme") - } - addrs = append(addrs, addr) - } - - var tlsConfig *tls.Config - if scheme != "http" && scheme != "https" { - return nil, fmt.Errorf("endpoints scheme must be http or https") +// NewKVStore returns a freshly initialized KVStore +func NewKVStore(ctx context.Context, cfg Config) (KVStore, error) { + ctx, logger := logging.GetLogComponent(ctx, logging.StoreComponent) + addrs, err := cfg.EndpointAddrs() + if err != nil { + return nil, err } - if scheme == "https" { - var err error - tlsConfig, err = common.NewTLSConfig(cfg.CertFile, cfg.KeyFile, cfg.CAFile, cfg.SkipTLSVerify) - if err != nil { - return nil, fmt.Errorf("cannot create store tls config: %v", err) - } + logger.Debug().Any("endpoints", addrs).Msg("") + tlsConfig, err := cfg.TLSConfig() + if err != nil { + return nil, err } + logger.Debug().Bool("tls enabled", tlsConfig != nil).Msg("") + var config valkeyrie.Config switch cfg.Backend { - case CONSUL, ETCDV2: - config := &libkvstore.Config{ + case CONSUL: + config = &consul.Config{ TLS: tlsConfig, ConnectionTimeout: cfg.Timeout, } - - store, err := libkv.NewStore(kvBackend, addrs, config) - if err != nil { - return nil, err - } - return &libKVStore{store: store}, nil - case ETCDV3: - config := etcdclientv3.Config{ - Endpoints: addrs, - TLS: tlsConfig, - DialTimeout: 20 * time.Second, - DialKeepAliveTime: 1 * time.Second, - DialKeepAliveTimeout: cfg.Timeout, + case ETCDV2: + config = &etcdv2.Config{ + TLS: tlsConfig, + ConnectionTimeout: cfg.Timeout, } - c, err := etcdclientv3.New(config) - if err != nil { - return nil, err + case ETCDV3: + config = &etcdv3.Config{ + TLS: tlsConfig, + ConnectionTimeout: cfg.Timeout, + SyncPeriod: cfg.Timeout, } - return &etcdV3Store{c: c, requestTimeout: cfg.Timeout}, nil default: return nil, fmt.Errorf("Unknown store backend: %q", cfg.Backend) } + logger.Debug().Any("store config", config).Msg("") + + store, err := valkeyrie.NewStore(ctx, cfg.Backend.string(), addrs, config) + if err != nil { + return nil, err + } + return &libKVStore{store: store}, nil } +// KVBackedStore defines a config store backed by a KV backend type KVBackedStore struct { clusterPath string store KVStore } +// NewKVBackedStore returns a freshly initialized KVBackedStore func NewKVBackedStore(kvStore KVStore, path string) *KVBackedStore { return &KVBackedStore{ clusterPath: path, @@ -212,7 +286,8 @@ func NewKVBackedStore(kvStore KVStore, path string) *KVBackedStore { } } -func (s *KVBackedStore) AtomicPutClusterData(ctx context.Context, cd *cluster.ClusterData, previous *KVPair) (*KVPair, error) { +// AtomicPutClusterData is an atomic way to store CLusterData to a kv store +func (s *KVBackedStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Data, previous *KVPair) (*KVPair, error) { cdj, err := json.Marshal(cd) if err != nil { return nil, err @@ -230,7 +305,8 @@ func (s *KVBackedStore) AtomicPutClusterData(ctx context.Context, cd *cluster.Cl return s.store.AtomicPut(ctx, path, cdj, prev, nil) } -func (s *KVBackedStore) PutClusterData(ctx context.Context, cd *cluster.ClusterData) error { +// PutClusterData stores ClusterData to a kv store +func (s *KVBackedStore) PutClusterData(ctx context.Context, cd *cluster.Data) error { cdj, err := json.Marshal(cd) if err != nil { return err @@ -239,8 +315,9 @@ func (s *KVBackedStore) PutClusterData(ctx context.Context, cd *cluster.ClusterD return s.store.Put(ctx, path, cdj, nil) } -func (s *KVBackedStore) GetClusterData(ctx context.Context) (*cluster.ClusterData, *KVPair, error) { - var cd *cluster.ClusterData +// GetClusterData retrieves ClusterData from a kv store +func (s *KVBackedStore) GetClusterData(ctx context.Context) (*cluster.Data, *KVPair, error) { + var cd *cluster.Data path := filepath.Join(s.clusterPath, clusterDataFile) pair, err := s.store.Get(ctx, path) if err != nil { @@ -255,17 +332,19 @@ func (s *KVBackedStore) GetClusterData(ctx context.Context) (*cluster.ClusterDat return cd, pair, nil } +// SetKeeperInfo stores keeper info to a kv store func (s *KVBackedStore) SetKeeperInfo(ctx context.Context, id string, ms *cluster.KeeperInfo, ttl time.Duration) error { msj, err := json.Marshal(ms) if err != nil { return err } - if ttl < MinTTL { - ttl = MinTTL + if ttl < minTTL { + ttl = minTTL } return s.store.Put(ctx, filepath.Join(s.clusterPath, keepersInfoDir, id), msj, &WriteOptions{TTL: ttl}) } +// GetKeepersInfo retrieves all keeper info from a kv store func (s *KVBackedStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, error) { keepers := cluster.KeepersInfo{} pairs, err := s.store.List(ctx, filepath.Join(s.clusterPath, keepersInfoDir)) @@ -286,17 +365,19 @@ func (s *KVBackedStore) GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo return keepers, nil } +// SetSentinelInfo stores info on a sentinel to the kv store func (s *KVBackedStore) SetSentinelInfo(ctx context.Context, si *cluster.SentinelInfo, ttl time.Duration) error { sij, err := json.Marshal(si) if err != nil { return err } - if ttl < MinTTL { - ttl = MinTTL + if ttl < minTTL { + ttl = minTTL } return s.store.Put(ctx, filepath.Join(s.clusterPath, sentinelsInfoDir, si.UID), sij, &WriteOptions{TTL: ttl}) } +// GetSentinelsInfo retrieves all sentinel info from a kv store func (s *KVBackedStore) GetSentinelsInfo(ctx context.Context) (cluster.SentinelsInfo, error) { ssi := cluster.SentinelsInfo{} pairs, err := s.store.List(ctx, filepath.Join(s.clusterPath, sentinelsInfoDir)) @@ -317,17 +398,19 @@ func (s *KVBackedStore) GetSentinelsInfo(ctx context.Context) (cluster.Sentinels return ssi, nil } +// SetProxyInfo stores info on a proxy to the kv store func (s *KVBackedStore) SetProxyInfo(ctx context.Context, pi *cluster.ProxyInfo, ttl time.Duration) error { pij, err := json.Marshal(pi) if err != nil { return err } - if ttl < MinTTL { - ttl = MinTTL + if ttl < minTTL { + ttl = minTTL } return s.store.Put(ctx, filepath.Join(s.clusterPath, proxiesInfoDir, pi.UID), pij, &WriteOptions{TTL: ttl}) } +// GetProxiesInfo retrieves all proxy info from a kv store func (s *KVBackedStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, error) { psi := cluster.ProxiesInfo{} pairs, err := s.store.List(ctx, filepath.Join(s.clusterPath, proxiesInfoDir)) @@ -348,11 +431,12 @@ func (s *KVBackedStore) GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo return psi, nil } +// NewKVBackedElection starts a campaign for getting elected with a kv backed store func NewKVBackedElection(kvStore KVStore, path, candidateUID string, timeout time.Duration) Election { switch kvStore := kvStore.(type) { case *libKVStore: s := kvStore - candidate := leadership.NewCandidate(s.store, path, candidateUID, MinTTL) + candidate := NewCandidate(s.store, path, candidateUID, minTTL) return &libkvElection{store: s, path: path, candidate: candidate} case *etcdV3Store: etcdV3Store := kvStore @@ -360,7 +444,7 @@ func NewKVBackedElection(kvStore KVStore, path, candidateUID string, timeout tim c: etcdV3Store.c, path: path, candidateUID: candidateUID, - ttl: MinTTL, + ttl: minTTL, requestTimeout: timeout, } default: diff --git a/internal/store/libkv.go b/internal/store/libkv.go index bd364e5f1..c0e05accf 100644 --- a/internal/store/libkv.go +++ b/internal/store/libkv.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,20 +15,14 @@ package store +// TODO: implement context + import ( "context" - "github.com/docker/leadership" - libkvstore "github.com/docker/libkv/store" - "github.com/docker/libkv/store/consul" - "github.com/docker/libkv/store/etcd" + libkvstore "github.com/kvtools/valkeyrie/store" ) -func init() { - etcd.Register() - consul.Register() -} - func fromLibKVStoreErr(err error) error { switch err { case libkvstore.ErrKeyNotFound: @@ -47,12 +42,12 @@ func (s *libKVStore) Put(ctx context.Context, key string, value []byte, options if options != nil { libkvOptions = &libkvstore.WriteOptions{TTL: options.TTL} } - err := s.store.Put(key, value, libkvOptions) + err := s.store.Put(ctx, key, value, libkvOptions) return fromLibKVStoreErr(err) } func (s *libKVStore) Get(ctx context.Context, key string) (*KVPair, error) { - pair, err := s.store.Get(key) + pair, err := s.store.Get(ctx, key, &libkvstore.ReadOptions{}) if err != nil { return nil, fromLibKVStoreErr(err) } @@ -60,7 +55,7 @@ func (s *libKVStore) Get(ctx context.Context, key string) (*KVPair, error) { } func (s *libKVStore) List(ctx context.Context, directory string) ([]*KVPair, error) { - pairs, err := s.store.List(directory) + pairs, err := s.store.List(ctx, directory, &libkvstore.ReadOptions{}) if err != nil { return nil, fromLibKVStoreErr(err) } @@ -71,7 +66,13 @@ func (s *libKVStore) List(ctx context.Context, directory string) ([]*KVPair, err return kvPairs, nil } -func (s *libKVStore) AtomicPut(ctx context.Context, key string, value []byte, previous *KVPair, options *WriteOptions) (*KVPair, error) { +func (s *libKVStore) AtomicPut( + ctx context.Context, + key string, + value []byte, + previous *KVPair, + options *WriteOptions, +) (*KVPair, error) { var libkvPrevious *libkvstore.KVPair if previous != nil { libkvPrevious = &libkvstore.KVPair{Key: previous.Key, LastIndex: previous.LastIndex} @@ -80,7 +81,7 @@ func (s *libKVStore) AtomicPut(ctx context.Context, key string, value []byte, pr if options != nil { libkvOptions = &libkvstore.WriteOptions{TTL: options.TTL} } - _, pair, err := s.store.AtomicPut(key, value, libkvPrevious, libkvOptions) + _, pair, err := s.store.AtomicPut(ctx, key, value, libkvPrevious, libkvOptions) if err != nil { return nil, fromLibKVStoreErr(err) } @@ -88,22 +89,22 @@ func (s *libKVStore) AtomicPut(ctx context.Context, key string, value []byte, pr } func (s *libKVStore) Delete(ctx context.Context, key string) error { - return fromLibKVStoreErr(s.store.Delete(key)) + return fromLibKVStoreErr(s.store.Delete(ctx, key)) } func (s *libKVStore) Close() error { - s.store.Close() - return nil + // TODO: implement zerolog + return s.store.Close() } type libkvElection struct { store *libKVStore path string - candidate *leadership.Candidate + candidate *Candidate } -func (e *libkvElection) RunForElection() (<-chan bool, <-chan error) { - return e.candidate.RunForElection() +func (e *libkvElection) RunForElection(ctx context.Context) (<-chan bool, <-chan error) { + return e.candidate.RunForElection(ctx) } func (e *libkvElection) Stop() { diff --git a/internal/store/store.go b/internal/store/store.go index 5776fa56e..2d6bbcff4 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,22 +20,25 @@ import ( "errors" "time" - "github.com/sorintlab/stolon/internal/cluster" + cluster "github.com/pgvillage-tools/stolon/api/v1" ) //go:generate mockgen -destination=../mock/store/store.go -source=$GOFILE var ( // ErrKeyNotFound is thrown when the key is not found in the store during a Get operation - ErrKeyNotFound = errors.New("Key not found in store") - ErrKeyModified = errors.New("Unable to complete atomic operation, key modified") + ErrKeyNotFound = errors.New("Key not found in store") + // ErrKeyModified is thrown when the key was modified while perfroming the atomic operation + ErrKeyModified = errors.New("Unable to complete atomic operation, key modified") + // ErrElectionNoLeader is thrown when there is no leader ErrElectionNoLeader = errors.New("election: no leader") ) +// Store is an interface for a kv store which could be consul, or etcd (v2 or v3) type Store interface { - AtomicPutClusterData(ctx context.Context, cd *cluster.ClusterData, previous *KVPair) (*KVPair, error) - PutClusterData(ctx context.Context, cd *cluster.ClusterData) error - GetClusterData(ctx context.Context) (*cluster.ClusterData, *KVPair, error) + AtomicPutClusterData(ctx context.Context, cd *cluster.Data, previous *KVPair) (*KVPair, error) + PutClusterData(ctx context.Context, cd *cluster.Data) error + GetClusterData(ctx context.Context) (*cluster.Data, *KVPair, error) SetKeeperInfo(ctx context.Context, id string, ms *cluster.KeeperInfo, ttl time.Duration) error GetKeepersInfo(ctx context.Context) (cluster.KeepersInfo, error) SetSentinelInfo(ctx context.Context, si *cluster.SentinelInfo, ttl time.Duration) error @@ -43,6 +47,7 @@ type Store interface { GetProxiesInfo(ctx context.Context) (cluster.ProxiesInfo, error) } +// Election takes care of the election proces with a kv backend type Election interface { // TODO(sgotti) this mimics the current docker/leadership API and the etcdv3 // implementations adapt to it. In future it could be replaced with a better @@ -52,7 +57,7 @@ type Election interface { // the consuming code calls election.Stop(). Failure to do so can cause // subsequent elections to hang indefinitely across all participants of an // election. - RunForElection() (<-chan bool, <-chan error) + RunForElection(context.Context) (<-chan bool, <-chan error) Leader() (string, error) Stop() } diff --git a/internal/timer/timer.go b/internal/timer/timer.go index 9d2da28c8..eec37d557 100644 --- a/internal/timer/timer.go +++ b/internal/timer/timer.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package timer is used as a generic interface for time and timing operations package timer import "time" diff --git a/internal/timer/timer_fallback.go b/internal/timer/timer_fallback.go index 510072618..7972b88cb 100644 --- a/internal/timer/timer_fallback.go +++ b/internal/timer/timer_fallback.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !linux // +build !linux package timer @@ -20,6 +22,7 @@ import ( "time" ) +// Now returns the current datetime as a Unix timestamp func Now() int64 { return time.Now().UnixNano() } diff --git a/internal/timer/timer_linux.go b/internal/timer/timer_linux.go index 3f6e9c3f9..2aa8d680a 100644 --- a/internal/timer/timer_linux.go +++ b/internal/timer/timer_linux.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build linux // +build linux package timer @@ -22,10 +24,11 @@ import ( ) const ( - // from /usr/include/linux/time.h + // CLOCK_MONOTONIC as defined in /usr/include/linux/time.h CLOCK_MONOTONIC = 1 ) +// Now uses a syscall to return the linux timestamp // TODO(sgotti) for the moment just use a syscall so it'll work on all linux // architectures. It's slower than using a vdso but we don't have such performance // needs. Let's wait for a stdlib native monotonic clock. diff --git a/internal/util/k8s.go b/internal/util/k8s.go index ce73de5e7..cfa498722 100644 --- a/internal/util/k8s.go +++ b/internal/util/k8s.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package util has all kind of util functions to support stolon package util import ( @@ -22,16 +24,25 @@ import ( ) const ( + // KubePodName defines the env variabele for a Pod when used in k8s KubePodName = "POD_NAME" + // KubeResourcePrefix is a hardcoded prefix for the configmap when running stolon on k8s KubeResourcePrefix = "stolon-cluster" + // KubeClusterLabel is a hardcoded label key to be set to the name of the cluster for all k8s resources belonging to + // the cluster KubeClusterLabel = "stolon-cluster" + // KubeClusterDataAnnotation is the key of the annotation that should be set on a ClusterData configmap KubeClusterDataAnnotation = "stolon-clusterdata" - KubeStatusAnnnotation = "stolon-status" + + // KubeStatusAnnnotation is the key of the annotation that should be set to define the status for every Pod for + // this cluster + KubeStatusAnnnotation = "stolon-status" ) +// PodName returns the name of the Pod from the env variabele func PodName() (string, error) { podName := os.Getenv(KubePodName) if len(podName) == 0 { diff --git a/internal/util/k8s_test.go b/internal/util/k8s_test.go new file mode 100644 index 000000000..0a19ad3c8 --- /dev/null +++ b/internal/util/k8s_test.go @@ -0,0 +1,113 @@ +// Copyright 2026 PgVillage +// Copyright 2018 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("k8s", func() { + Describe("PodName", func() { + var originalPodName string + + BeforeEach(func() { + originalPodName = os.Getenv(KubePodName) + }) + + AfterEach(func() { + os.Setenv(KubePodName, originalPodName) + }) + + Context("when the POD_NAME environment variable is set", func() { + It("should return the pod name", func() { + expectedPodName := "my-pod" + os.Setenv(KubePodName, expectedPodName) + podName, err := PodName() + Expect(err).NotTo(HaveOccurred()) + Expect(podName).To(Equal(expectedPodName)) + }) + }) + + Context("when the POD_NAME environment variable is not set", func() { + It("should return an error", func() { + os.Unsetenv(KubePodName) + _, err := PodName() + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("NewKubeClientConfig", func() { + It("should create a client config with the provided parameters", func() { + kubeconfigContent := ` +apiVersion: v1 +clusters: +- cluster: + server: https://server1:8443 + name: cluster1 +- cluster: + server: https://server2:8443 + name: cluster2 +contexts: +- context: + cluster: cluster1 + user: user1 + name: context1 +- context: + cluster: cluster2 + user: user2 + name: context2 +current-context: context1 +kind: Config +preferences: {} +users: +- name: user1 + user: + token: token1 +- name: user2 + user: + token: token2 +` + kubeconfigFile, err := os.CreateTemp("", "kubeconfig") + Expect(err).NotTo(HaveOccurred()) + defer os.Remove(kubeconfigFile.Name()) + + _, err = kubeconfigFile.Write([]byte(kubeconfigContent)) + Expect(err).NotTo(HaveOccurred()) + err = kubeconfigFile.Close() + Expect(err).NotTo(HaveOccurred()) + + kubeconfigPath := kubeconfigFile.Name() + context := "context2" + namespace := "my-namespace" + + clientConfig := NewKubeClientConfig(kubeconfigPath, context, namespace) + + // verify namespace + ns, _, err := clientConfig.Namespace() + Expect(err).NotTo(HaveOccurred()) + Expect(ns).To(Equal(namespace)) + + // verify that the correct context is used + restConfig, err := clientConfig.ClientConfig() + Expect(err).NotTo(HaveOccurred()) + Expect(restConfig.Host).To(Equal("https://server2:8443")) + }) + }) +}) diff --git a/internal/util/main.go b/internal/util/main.go new file mode 100644 index 000000000..eedc9c33d --- /dev/null +++ b/internal/util/main.go @@ -0,0 +1,43 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "reflect" + + "github.com/mitchellh/copystructure" +) + +// ToPtr returns the value as a pointer +func ToPtr[T any](v T) *T { + return &v +} + +// DeepCopy returns a copy of the KeeperInfo resource +func DeepCopy[T any](org *T) (dc *T) { + var ok bool + if org == nil { + return nil + } + if nk, err := copystructure.Copy(org); err != nil { + panic(err) + } else if !reflect.DeepEqual(org, nk) { + panic("not equal") + } else if dc, ok = nk.(*T); !ok { + panic("different type after copy") + } + return dc +} diff --git a/internal/util/main_test.go b/internal/util/main_test.go new file mode 100644 index 000000000..2a7fa29d9 --- /dev/null +++ b/internal/util/main_test.go @@ -0,0 +1,38 @@ +package util + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Main", func() { + Describe("ToPtr", func() { + Context("with an integer", func() { + It("should return a pointer to the integer", func() { + val := 42 + ptr := ToPtr(val) + Expect(*ptr).To(Equal(val)) + }) + }) + + Context("with a string", func() { + It("should return a pointer to the string", func() { + val := "hello" + ptr := ToPtr(val) + Expect(*ptr).To(Equal(val)) + }) + }) + + Context("with a struct", func() { + It("should return a pointer to the struct", func() { + type myStruct struct { + Field1 string + Field2 int + } + val := myStruct{Field1: "test", Field2: 123} + ptr := ToPtr(val) + Expect(*ptr).To(Equal(val)) + }) + }) + }) +}) diff --git a/internal/util/slice.go b/internal/util/slice.go index d66be5dca..b524d8caf 100644 --- a/internal/util/slice.go +++ b/internal/util/slice.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +17,7 @@ package util import "sort" +// StringInSlice checks for a string in a string slice func StringInSlice(s []string, e string) bool { for _, v := range s { if v == e { @@ -39,7 +41,8 @@ func CompareStringSlice(a []string, b []string) bool { return true } -// CompareStringSliceNoOrder compares two slices of strings regardless of their order, a nil slice is considered an empty one +// CompareStringSliceNoOrder compares two slices of strings regardless of their order, a nil slice is considered an +// empty one func CompareStringSliceNoOrder(a []string, b []string) bool { if len(a) != len(b) { return false diff --git a/internal/util/slice_test.go b/internal/util/slice_test.go index 80bcc05f6..7cd5f0dda 100644 --- a/internal/util/slice_test.go +++ b/internal/util/slice_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -8,92 +9,91 @@ // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package util -import "testing" +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) -func TestCompareStringSlice(t *testing.T) { - tests := []struct { - a []string - b []string - ok bool - }{ - {[]string{}, []string{}, true}, - {[]string{"", ""}, []string{""}, false}, - {[]string{"", ""}, []string{"", ""}, true}, - {[]string{"a", "b"}, []string{"a", "b"}, true}, - {[]string{"a", "b"}, []string{"b", "a"}, false}, - {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, - {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, - {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, false}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, - } +var _ = Describe("Slice", func() { + Describe("CompareStringSlice", func() { + It("should correctly compare string slices", func() { + tests := []struct { + a []string + b []string + ok bool + }{ + {[]string{}, []string{}, true}, + {[]string{"", ""}, []string{""}, false}, + {[]string{"", ""}, []string{"", ""}, true}, + {[]string{"a", "b"}, []string{"a", "b"}, true}, + {[]string{"a", "b"}, []string{"b", "a"}, false}, + {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, + {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, false}, + {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, + } - for i, tt := range tests { - ok := CompareStringSlice(tt.a, tt.b) - if ok != tt.ok { - t.Errorf("%d: got %t but wanted: %t a: %v, b: %v", i, ok, tt.ok, tt.a, tt.b) - } - } -} + for _, tt := range tests { + Expect(CompareStringSlice(tt.a, tt.b)).To(Equal(tt.ok)) + } + }) + }) -func TestCompareStringSliceNoOrder(t *testing.T) { - tests := []struct { - a []string - b []string - ok bool - }{ - {[]string{}, []string{}, true}, - {[]string{"", ""}, []string{""}, false}, - {[]string{"", ""}, []string{"", ""}, true}, - {[]string{"a", "b"}, []string{"a", "b"}, true}, - {[]string{"a", "b"}, []string{"b", "a"}, true}, - {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, - {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, - {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, true}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, - } + Describe("CompareStringSliceNoOrder", func() { + It("should correctly compare string slices ignoring order", func() { + tests := []struct { + a []string + b []string + ok bool + }{ + {[]string{}, []string{}, true}, + {[]string{"", ""}, []string{""}, false}, + {[]string{"", ""}, []string{"", ""}, true}, + {[]string{"a", "b"}, []string{"a", "b"}, true}, + {[]string{"a", "b"}, []string{"b", "a"}, true}, + {[]string{"a", "b", "c"}, []string{"a", "b"}, false}, + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, true}, + {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, true}, + {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, false}, + } - for i, tt := range tests { - ok := CompareStringSliceNoOrder(tt.a, tt.b) - if ok != tt.ok { - t.Errorf("%d: got %t but wanted: %t a: %v, b: %v", i, ok, tt.ok, tt.a, tt.b) - } - } -} + for _, tt := range tests { + Expect(CompareStringSliceNoOrder(tt.a, tt.b)).To(Equal(tt.ok)) + } + }) + }) -func TestDifference(t *testing.T) { - tests := []struct { - a []string - b []string - r []string - }{ - {[]string{}, []string{}, []string{}}, - {[]string{"", ""}, []string{""}, []string{}}, - {[]string{"", ""}, []string{"", ""}, []string{}}, - {[]string{"", ""}, []string{"a", "", "b"}, []string{}}, - {[]string{"a", "b"}, []string{"a", "b"}, []string{}}, - {[]string{"a", "b"}, []string{"b", "a"}, []string{}}, - {[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}}, - {[]string{"a", "b", "c"}, []string{"a", "b"}, []string{"c"}}, - {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{}}, - {[]string{"a", "b"}, []string{"c", "a", "b"}, []string{}}, - {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, []string{}}, - {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, []string{}}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, []string{}}, - {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, []string{}}, - } + Describe("Difference", func() { + It("should return the difference between two string slices", func() { + tests := []struct { + a []string + b []string + r []string + }{ + {[]string{}, []string{}, []string{}}, + {[]string{"", ""}, []string{""}, []string{}}, + {[]string{"", ""}, []string{"", ""}, []string{}}, + {[]string{"", ""}, []string{"a", "", "b"}, []string{}}, + {[]string{"a", "b"}, []string{"a", "b"}, []string{}}, + {[]string{"a", "b"}, []string{"b", "a"}, []string{}}, + {[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}}, + {[]string{"a", "b", "c"}, []string{"a", "b"}, []string{"c"}}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{}}, + {[]string{"a", "b"}, []string{"c", "a", "b"}, []string{}}, + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}, []string{}}, + {[]string{"a", "b", "c"}, []string{"b", "c", "a"}, []string{}}, + {[]string{"a", "b", "c", "a"}, []string{"a", "c", "b", "b"}, []string{}}, + } - for i, tt := range tests { - r := Difference(tt.a, tt.b) - if !CompareStringSliceNoOrder(r, tt.r) { - t.Errorf("%d: got %v but wanted: %v a: %v, b: %v", i, r, tt.r, tt.a, tt.b) - } - } -} + for _, tt := range tests { + Expect(Difference(tt.a, tt.b)).To(ConsistOf(tt.r)) + } + }) + }) +}) diff --git a/internal/util/user.go b/internal/util/user.go index 7a5f9c7c3..9585b96ad 100644 --- a/internal/util/user.go +++ b/internal/util/user.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +16,16 @@ package util import ( - "fmt" + "errors" "os" "os/user" ) +var currentUserFunc = user.Current + +// GetUser returns the linux username running stolon func GetUser() (string, error) { - u, err := user.Current() + u, err := currentUserFunc() if err == nil { return u.Username, nil } @@ -31,5 +35,5 @@ func GetUser() (string, error) { return name, nil } - return "", fmt.Errorf("cannot detect current user") + return "", errors.New("cannot detect current user") } diff --git a/internal/util/user_test.go b/internal/util/user_test.go new file mode 100644 index 000000000..7810a2cc4 --- /dev/null +++ b/internal/util/user_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 PgVillage +// Copyright 2016 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "errors" + "os" + "os/user" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("User", func() { + var originalUserEnv string + var originalCurrentUserFunc func() (*user.User, error) + + BeforeEach(func() { + originalUserEnv = os.Getenv("USER") + originalCurrentUserFunc = currentUserFunc + }) + + AfterEach(func() { + os.Setenv("USER", originalUserEnv) + currentUserFunc = originalCurrentUserFunc + }) + + Context("when user.Current() succeeds", func() { + BeforeEach(func() { + currentUserFunc = func() (*user.User, error) { + return &user.User{Username: "mockuser_current"}, nil + } + }) + + It("should return the username from user.Current()", func() { + os.Setenv("USER", "env_user") // This should be ignored + username, err := GetUser() + Expect(err).NotTo(HaveOccurred()) + Expect(username).To(Equal("mockuser_current")) + }) + }) + + Context("when user.Current() fails and USER environment variable is set", func() { + BeforeEach(func() { + currentUserFunc = func() (*user.User, error) { + return nil, errors.New("mock user.Current() error") + } + }) + + It("should return the username from the USER environment variable", func() { + expectedUser := "env_user" + os.Setenv("USER", expectedUser) + username, err := GetUser() + Expect(err).NotTo(HaveOccurred()) + Expect(username).To(Equal(expectedUser)) + }) + }) + + Context("when user.Current() fails and USER environment variable is not set", func() { + BeforeEach(func() { + currentUserFunc = func() (*user.User, error) { + return nil, errors.New("mock user.Current() error") + } + }) + + It("should return an error", func() { + os.Unsetenv("USER") + username, err := GetUser() + Expect(username).To(BeEmpty()) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("cannot detect current user")) + }) + }) +}) diff --git a/internal/util/util_suite_test.go b/internal/util/util_suite_test.go new file mode 100644 index 000000000..4c6eb3f25 --- /dev/null +++ b/internal/util/util_suite_test.go @@ -0,0 +1,13 @@ +package util + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestUtil(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Util Suite") +} diff --git a/scripts/agola-k8s.sh b/scripts/agola-k8s.sh index 7c506cfd1..694477c91 100755 --- a/scripts/agola-k8s.sh +++ b/scripts/agola-k8s.sh @@ -6,8 +6,8 @@ apk add make jq echo -n "Waiting for docker to be ready" until curl -s --fail http://127.0.0.1:10080/docker-ready; do - sleep 1; - echo -n "." + sleep 1 + echo -n "." done echo " Ready" @@ -19,14 +19,14 @@ pushd examples/kubernetes echo -n "Waiting for kubernetes to be ready" until curl -s --fail http://127.0.0.1:10080/kubernetes-ready; do - sleep 1; - echo -n "." + sleep 1 + echo -n "." done echo " Ready" -sed -i 's#sorintlab/stolon:master-pg10#stolon:master-pg11#' *.yaml +sed -i 's#pgvillage-tools/stolon-keeper:17#spgvillage-tools/stolon-keeper:16#' ./*.yaml -for i in role.yaml role-binding.yaml secret.yaml stolon-sentinel.yaml stolon-keeper.yaml stolon-proxy.yaml stolon-proxy-service.yaml ; do +for i in role.yaml role-binding.yaml secret.yaml stolon-sentinel.yaml stolon-keeper.yaml stolon-proxy.yaml stolon-proxy-service.yaml; do kubectl apply -f $i done diff --git a/scripts/build-binary b/scripts/build-binary index 54c197065..b97b1a330 100755 --- a/scripts/build-binary +++ b/scripts/build-binary @@ -6,7 +6,7 @@ VER=$1 PROJ="stolon" if [ -z "$1" ]; then - echo "Usage: ${0} VERSION" >> /dev/stderr + echo "Usage: ${0} VERSION" >>/dev/stderr exit 255 fi @@ -16,67 +16,67 @@ function setup_env { local proj=${1} local ver=${2} - if [ ! -d ${proj} ]; then - git clone https://github.com/sorintlab/${proj} + if [ ! -d "${proj}" ]; then + git clone https://github.com/pgvillage-tools/"${proj}" fi - pushd ${proj} >/dev/null - git checkout master - git fetch --all - git reset --hard origin/master - git checkout $ver + pushd "${proj}" >/dev/null + git checkout master + git fetch --all + git reset --hard origin/master + git checkout "$ver" popd >/dev/null } - function package { local target=${1} local srcdir="${2}/bin" local ccdir="${srcdir}/${GOOS}_${GOARCH}" - if [ -d ${ccdir} ]; then - srcdir=${ccdir} + if [ -d "${ccdir}" ]; then + srcdir="${ccdir}" fi - mkdir ${target}/bin + mkdir "${target}"/bin for bin in stolon-keeper stolon-sentinel stolon-proxy stolonctl; do - cp ${srcdir}/${bin} ${target}/bin + cp "${srcdir}/${bin}" "${target}/bin" done - cp stolon/README.md ${target}/README.md + cp stolon/README.md "${target}/README.md" - cp -R stolon/doc ${target}/doc - cp -R stolon/examples ${target}/examples - rm -rf ${target}/examples/kubernetes/image/docker/bin - rm -rf ${target}/examples/docker/bin + cp -R stolon/doc "${target}/doc" + cp -R stolon/examples "${target}/examples" + rm -rf "${target}/examples/kubernetes/image/docker/bin" + rm -rf "${target}/examples/docker/bin" } function main { mkdir -p release cd release - setup_env ${PROJ} ${VER} - - for os in linux; do - export GOOS=${os} - export GOARCH="amd64" - - pushd stolon >/dev/null - make - popd >/dev/null - - TARGET="stolon-${VER}-${GOOS}-${GOARCH}" - mkdir -p ${TARGET} - package ${TARGET} ${PROJ} - - if [ ${GOOS} == "linux" ]; then - tar cfz ${TARGET}.tar.gz ${TARGET} - echo "Wrote release/${TARGET}.tar.gz" - else - zip -qr ${TARGET}.zip ${TARGET} - echo "Wrote release/${TARGET}.zip" - fi - done + setup_env "${PROJ}" "${VER}" + + #for os in linux; do + # export GOOS=${os} + export GOOS=linux + export GOARCH="amd64" + + pushd stolon >/dev/null + make + popd >/dev/null + + TARGET="stolon-${VER}-${GOOS}-${GOARCH}" + mkdir -p "${TARGET}" + package "${TARGET}" "${PROJ}" + + #if [ ${GOOS} == "linux" ]; then + tar cfz "${TARGET}.tar.gz" "${TARGET}" + echo "Wrote release/${TARGET}.tar.gz" + #else + # zip -qr "${TARGET}.zip" "${TARGET}" + # echo "Wrote release/${TARGET}.zip" + #fi + #done } main diff --git a/scripts/gen_commands_doc.go b/scripts/gen_commands_doc.go index 12122c78e..daf4fcefb 100644 --- a/scripts/gen_commands_doc.go +++ b/scripts/gen_commands_doc.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2018 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package main is a package that provides functionality concerning postgresSQL lifecycles package main import ( @@ -19,10 +21,10 @@ import ( "log" "os" - keepercmd "github.com/sorintlab/stolon/cmd/keeper/cmd" - proxycmd "github.com/sorintlab/stolon/cmd/proxy/cmd" - sentinelcmd "github.com/sorintlab/stolon/cmd/sentinel/cmd" - stolonctlcmd "github.com/sorintlab/stolon/cmd/stolonctl/cmd" + keepercmd "github.com/pgvillage-tools/stolon/cmd/keeper/cmd" + proxycmd "github.com/pgvillage-tools/stolon/cmd/proxy/cmd" + sentinelcmd "github.com/pgvillage-tools/stolon/cmd/sentinel/cmd" + stolonctlcmd "github.com/pgvillage-tools/stolon/cmd/stolonctl/cmd" "github.com/spf13/cobra/doc" ) @@ -30,12 +32,11 @@ import ( func main() { // use os.Args instead of "flags" because "flags" will mess up the man pages! var outDir string - if len(os.Args) == 2 { - outDir = os.Args[1] - } else { + if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "usage: %s [output directory]", os.Args[0]) os.Exit(1) } + outDir = os.Args[1] if err := doc.GenMarkdownTree(keepercmd.CmdKeeper, outDir); err != nil { log.Fatal(err) diff --git a/scripts/gen_commands_doc.sh b/scripts/gen_commands_doc.sh index 8a49b4a09..061bfd421 100755 --- a/scripts/gen_commands_doc.sh +++ b/scripts/gen_commands_doc.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash set -e -source $(dirname $0)/readlinkdashf.sh +# shellcheck disable=SC1091 +source "$(dirname "$0")"/readlinkdashf.sh -BASEDIR=$(readlinkdashf $(dirname $0)/..) +BASEDIR=$(readlinkdashf "$(dirname "$0")"/..) -go run $BASEDIR/scripts/gen_commands_doc.go $BASEDIR/doc/commands/ +go run "$BASEDIR"/scripts/gen_commands_doc.go "$BASEDIR"/doc/commands/ diff --git a/scripts/git-version.sh b/scripts/git-version.sh index a60cdc405..a9a19229f 100755 --- a/scripts/git-version.sh +++ b/scripts/git-version.sh @@ -1,21 +1,21 @@ #!/bin/sh -e # parse the current git commit hash -COMMIT=`git rev-parse HEAD` +COMMIT=$(git rev-parse HEAD) # check if the current commit has a matching tag -TAG=$(git describe --exact-match --abbrev=0 --tags ${COMMIT} 2> /dev/null || true) +TAG=$(git describe --exact-match --abbrev=0 --tags "${COMMIT}" 2>/dev/null || true) # use the matching tag as the version, if available if [ -z "$TAG" ]; then - VERSION=$COMMIT + VERSION=$COMMIT else - VERSION=$TAG + VERSION=$TAG fi # check for changed files (not untracked files) -if [ -n "$(git diff --shortstat 2> /dev/null | tail -n1)" ]; then - VERSION="${VERSION}-dirty" +if [ -n "$(git diff --shortstat 2>/dev/null | tail -n1)" ]; then + VERSION="${VERSION}-dirty" fi -echo $VERSION +echo "$VERSION" diff --git a/scripts/readlinkdashf.sh b/scripts/readlinkdashf.sh index aea707385..db86140ab 100755 --- a/scripts/readlinkdashf.sh +++ b/scripts/readlinkdashf.sh @@ -1,20 +1,21 @@ +#!/usr/bin/env bash # Cross compatibility with osx # origin source: https://github.com/kubernetes/kubernetes/ blob/master/hack/lib/init.sh#L102 function readlinkdashf() { - # run in a subshell for simpler 'cd' - ( - if [[ -d "$1" ]]; then # This also catch symlinks to dirs. - cd "$1" - pwd -P - else - cd $(dirname "$1") - local f - f=$(basename "$1") - if [[ -L "$f" ]]; then - readlink "$f" - else - echo "$(pwd -P)/${f}" - fi - fi - ) + # run in a subshell for simpler 'cd' + ( + if [[ -d "$1" ]]; then # This also catch symlinks to dirs. + cd "$1" || return 1 + pwd -P + else + cd "$(dirname "$1")" || return 1 + local f + f=$(basename "$1") + if [[ -L "$f" ]]; then + readlink "$f" + else + echo "$(pwd -P)/${f}" + fi + fi + ) } diff --git a/scripts/release.sh b/scripts/release.sh index 7ea331eec..b2408e72c 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -7,17 +7,15 @@ set -e VERSION=$1 if [ -z "${VERSION}" ]; then - echo "Usage: ${0} VERSION" >> /dev/stderr + echo "Usage: ${0} VERSION" >>/dev/stderr exit 255 fi -BASEDIR=$(readlink -f $(dirname $0))/.. -BINDIR=${BASEDIR}/bin +BASEDIR=$(readlink -f "$(dirname "$0")")/.. -if [ $PWD != $BASEDIR ]; then - cd $BASEDIR +if [ "$PWD" != "$BASEDIR" ]; then + cd "$BASEDIR" fi - echo Building stolon binary... -./scripts/build-binary ${VERSION} +./scripts/build-binary "${VERSION}" diff --git a/test b/test deleted file mode 100755 index 57bdb6749..000000000 --- a/test +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash - -# Test script code thanks to coreos/etcd -# -# Run all tests -# ./test -# ./test -v -# -# Run also integration tests -# INTEGRATION=1 STOLON_TEST_STORE_BACKEND=etcdv3 ./test -# -set -e - -source $(dirname $0)/scripts/readlinkdashf.sh -BASEDIR=$(readlinkdashf $(dirname $0)) -BINDIR=${BASEDIR}/bin - -if [ $PWD != $BASEDIR ]; then - cd $BASEDIR -fi - -ORG_PATH="github.com/sorintlab" -REPO_PATH="${ORG_PATH}/stolon" - -# test all packages excluding integration tests -IGNORE_PKGS="(vendor/|tests/integration)" -PACKAGES=$(find . -name \*_test.go | while read -r a; do dirname "$a"; done | sort | uniq | grep -vE "$IGNORE_PKGS" | sed "s|\./||g") - -# prepend REPO_PATH to each local package -split=$PACKAGES -PACKAGES="" -for a in $split; do PACKAGES="$PACKAGES ${REPO_PATH}/${a}"; done - -echo "Running tests..." - -COVER=${COVER:-"-cover"} - -## Use this code in case we are testing with multiple go versions that have different gofmt outputs -# # Use only of specific go version to run gofmt since it'll break when some formatting rules change between versions -# GOFMT_VERSION="go1.14" -# MAJOR_GOVERSION=$( echo -n $(go version) | grep -o 'go1\.[0-9]*' || true ) -# if [ "${MAJOR_GOVERSION}" == "${GOFMT_VERSION}" ]; then -# echo "Checking gofmt..." -# fmtRes=$(gofmt -l $(find . -type f -name '*.go' ! -path './vendor/*' ! -path '*/\.*')) -# if [ -n "${fmtRes}" ]; then -# echo -e "gofmt checking failed:\n${fmtRes}" -# exit 255 -# fi -# fi - -echo "Checking gofmt..." -fmtRes=$(gofmt -l $(find . -type f -name '*.go' ! -path './vendor/*' ! -path '*/\.*')) -if [ -n "${fmtRes}" ]; then - echo -e "gofmt checking failed:\n${fmtRes}" - exit 255 -fi - -echo "Checking govet..." -vetRes=$(go vet ${PACKAGES}) -if [ -n "${vetRes}" ]; then - echo -e "govet checking failed:\n${vetRes}" - exit 255 -fi - -echo "Checking govet -shadow ..." -go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow -export PATH="$(go env GOPATH)/bin":${PATH} -shadow_tool=$(which shadow) -vetRes=$(${shadow_tool} ${PACKAGES}) -if [ -n "${vetRes}" ]; then - echo -e "govet checking ${path} failed:\n${vetRes}" - exit 255 -fi - -echo "Checking for license header..." -licRes=$(for file in $(find . -type f -iname '*.go' ! -path './vendor/*'); do - head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}" - done;) -if [ -n "${licRes}" ]; then - echo -e "license header checking failed:\n${licRes}" - exit 255 -fi - - -echo "Checking with golangci-lint" -curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b ${BINDIR} v1.23.6 -${BINDIR}/golangci-lint run --deadline 5m - -echo "Running tests" -go test -timeout 3m ${COVER} $@ ${PACKAGES} ${RACE} - -if [ -n "$INTEGRATION" ]; then - echo "Running integration tests..." - if [ -z ${STOLON_TEST_STORE_BACKEND} ]; then - echo "STOLON_TEST_STORE_BACKEND env var needs to be defined (etcd or consul)" - exit 1 - fi - export STKEEPER_BIN=${BINDIR}/stolon-keeper - export STSENTINEL_BIN=${BINDIR}/stolon-sentinel - export STPROXY_BIN=${BINDIR}/stolon-proxy - export STCTL_BIN=${BINDIR}/stolonctl - if [ "${STOLON_TEST_STORE_BACKEND}" == "etcd" -o "${STOLON_TEST_STORE_BACKEND}" == "etcdv2" -o "${STOLON_TEST_STORE_BACKEND}" == "etcdv3" ]; then - if [ -z ${ETCD_BIN} ]; then - if [ -z $(which etcd) ]; then - echo "cannot find etcd in PATH and ETCD_BIN environment variable not defined" - exit 1 - fi - ETCD_BIN=$(which etcd) - fi - echo "using etcd from $ETCD_BIN" - export ETCD_BIN - elif [ "${STOLON_TEST_STORE_BACKEND}" == "consul" ]; then - if [ -z ${CONSUL_BIN} ]; then - if [ -z $(which consul) ]; then - echo "cannot find consul in PATH and CONSUL_BIN environment variable not defined" - exit 1 - fi - CONSUL_BIN=$(which consul) - fi - echo "using consul from $CONSUL_BIN" - export CONSUL_BIN - else - echo "Unknown store backend: \"${STOLON_TEST_STORE_BACKEND}\"" - exit 1 - fi - - [ -z "$PARALLEL" ] && PARALLEL=4 - go test -timeout 20m $@ -v -count 1 -parallel ${PARALLEL} ${REPO_PATH}/tests/integration -fi - -echo "Success" diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..f0f2bff96 --- /dev/null +++ b/test.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +# Test script code thanks to coreos/etcd +# +# Run all tests +# ./test +# ./test -v +# +# Run also integration tests +# INTEGRATION=1 STOLON_TEST_STORE_BACKEND=etcdv3 ./test +# +set -e + +# shellcheck disable=SC1091 +source "$(dirname "$0")"/scripts/readlinkdashf.sh +BASEDIR=$(readlinkdashf "$(dirname "$0")") +BINDIR=${BASEDIR}/bin + +if [ "$PWD" != "$BASEDIR" ]; then + cd "$BASEDIR" +fi + +ORG_PATH="github.com/pgvillage-tools" +REPO_PATH="${ORG_PATH}/stolon" + +echo "Running tests..." + +COVER=${COVER:-"-cover"} + +function go_files_without_license_info() { + find . -type f -iname '*.go' ! -path './vendor/*' ! -path './tests/*' ! -name '*_test.go' | while read -r file; do + head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}" + done +} + +echo "Checking for license header..." +IFS=$'\n' read -r -d '' -a licRes < <(go_files_without_license_info) || true +if ((${#licRes[@]})); then + echo -e "license header checking failed:\n${licRes[*]}" + exit 255 +fi + +echo "Running go test" +# test all packages excluding integration tests +IGNORE_PKGS="(vendor/|tests/integration)" + +function find_packages() { + find "." -name \*_test.go | while read -r a; do dirname "$a"; done | sort | uniq | grep -vE "$IGNORE_PKGS" | sed "s|^\.|${REPO_PATH}|g" +} +IFS=$'\n' read -r -d '' -a PACKAGES < <(find_packages) || true + +go test -timeout 3m "${COVER}" "$@" "${PACKAGES[@]}" "${RACE[@]}" + +if [ -n "$INTEGRATION" ]; then + echo "Running integration tests..." + if [ -z "${STOLON_TEST_STORE_BACKEND}" ]; then + echo "STOLON_TEST_STORE_BACKEND env var needs to be defined (etcd or consul)" + exit 1 + fi + export STKEEPER_BIN=${BINDIR}/stolon-keeper + export STSENTINEL_BIN=${BINDIR}/stolon-sentinel + export STPROXY_BIN=${BINDIR}/stolon-proxy + export STCTL_BIN=${BINDIR}/stolonctl + if [ "${STOLON_TEST_STORE_BACKEND}" == "etcd" ] || [ "${STOLON_TEST_STORE_BACKEND}" == "etcdv2" ] || [ "${STOLON_TEST_STORE_BACKEND}" == "etcdv3" ]; then + if [ -z "${ETCD_BIN}" ]; then + if [ -z "$(which etcd)" ]; then + echo "cannot find etcd in PATH and ETCD_BIN environment variable not defined" + exit 1 + fi + ETCD_BIN=$(which etcd) + fi + echo "using etcd from $ETCD_BIN" + export ETCD_BIN + elif [ "${STOLON_TEST_STORE_BACKEND}" == "consul" ]; then + if [ -z "${CONSUL_BIN}" ]; then + if [ -z "$(which consul)" ]; then + echo "cannot find consul in PATH and CONSUL_BIN environment variable not defined" + exit 1 + fi + CONSUL_BIN=$(which consul) + fi + echo "using consul from $CONSUL_BIN" + export CONSUL_BIN + else + echo "Unknown store backend: \"${STOLON_TEST_STORE_BACKEND}\"" + exit 1 + fi + + [ -z "$PARALLEL" ] && PARALLEL=4 + go test -timeout 20m "$@" -v -count 1 -parallel "${PARALLEL}" "${REPO_PATH}"/tests/integration +fi + +echo "Success" diff --git a/tests/etcdv3/container_utils.go b/tests/etcdv3/container_utils.go new file mode 100644 index 000000000..8e1d87b9f --- /dev/null +++ b/tests/etcdv3/container_utils.go @@ -0,0 +1,154 @@ +package etcdv3_test + +import ( + "context" + "fmt" + "os" + "strings" + + . "github.com/onsi/ginkgo/v2" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/etcd" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + etcdImage = "quay.io/coreos/etcd:v3.6.7" + proxyPort = 25432 + keeperPort = 5432 + pgUser = "postgres" + pgDatabase = "postgres" +) + +func runEtcd( + ctx context.Context, + etcdImage string, + nw *testcontainers.DockerNetwork, +) ( + cnt *etcd.EtcdContainer, + ep string, + err error, +) { + // setup etcd + etcdContainer, startErr := etcd.Run(ctx, + etcdImage, + network.WithNetwork([]string{"etcd"}, nw), + etcd.WithAdditionalArgs( + "--advertise-client-urls", "http://etcd:2379", + "--initial-advertise-peer-urls", "http://etcd:2380", + "--initial-cluster", "default=http://etcd:2380", + ), + ) + if startErr != nil { + return nil, "", startErr + } + ips, ipErr := etcdContainer.ContainerIPs(ctx) + if ipErr != nil { + return nil, "", ipErr + } + return etcdContainer, fmt.Sprintf("http://%s:2379", ips[0]), nil +} + +func runStolonCtl( + ctx context.Context, + etcdEndpoints string, + nw *testcontainers.DockerNetwork, + command ...string, +) (testcontainers.Container, error) { + return testcontainers.GenericContainer( + ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Cmd: command, + Env: map[string]string{ + "STOLONCTL_STORE_ENDPOINTS": etcdEndpoints, + "STOLONCTL_LOG_LEVEL": "debug", + }, + Networks: []string{nw.Name}, + Image: "stolonctl", + }, + Started: true, + }) +} + +func runKeeper( + ctx context.Context, + etcdEndpoints string, + nw *testcontainers.DockerNetwork, + aliasses map[string][]string, + settings map[string]string, +) (testcontainers.Container, error) { + pgVersion := os.Getenv("PGVERSION") + if pgVersion == "" { + pgVersion = "18" + } + envSettings := map[string]string{ + "STKEEPER_STORE_ENDPOINTS": etcdEndpoints, + "STKEEPER_LOG_LEVEL": "debug", + } + for k, v := range settings { + k = fmt.Sprintf("STKEEPER_%s", + strings.ReplaceAll(strings.ToUpper(k), "-", "_")) + envSettings[k] = v + } + image := fmt.Sprintf("keeper-%s", pgVersion) + fmt.Fprintf(GinkgoWriter, "DEBUG - Keeper image: %s", image) + return testcontainers.GenericContainer( + ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Env: envSettings, + Image: image, + Networks: []string{nw.Name}, + NetworkAliases: aliasses, + WaitingFor: wait.ForLog( + "postgres hba entries not changed"), + }, + Started: true, + }) +} + +func runSentinel( + ctx context.Context, + etcdEndpoints string, + nw *testcontainers.DockerNetwork, +) (testcontainers.Container, error) { + return testcontainers.GenericContainer( + ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Env: map[string]string{ + "STSENTINEL_STORE_ENDPOINTS": etcdEndpoints, + "STSENTINEL_LOG_LEVEL": "debug", + }, + Networks: []string{nw.Name}, + ExtraHosts: []string{}, + Image: "sentinel", + }, + Started: true, + }) +} + +func runProxy( + ctx context.Context, + etcdEndpoints string, + nw *testcontainers.DockerNetwork, + aliasses map[string][]string, +) (testcontainers.Container, error) { + envSettings := map[string]string{ + "STPROXY_STORE_ENDPOINTS": etcdEndpoints, + "STPROXY_LOG_LEVEL": "debug", + } + return testcontainers.GenericContainer( + ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Env: envSettings, + Image: "proxy", + Networks: []string{nw.Name}, + NetworkAliases: aliasses, + WaitingFor: wait.ForLog( + "proxying to master address"), + // WaitingFor: wait.ForListeningPort( + // nat.Port(fmt.Sprintf("%d/tcp", proxyPort))), + }, + Started: true, + }) +} diff --git a/tests/etcdv3/postgres_utils.go b/tests/etcdv3/postgres_utils.go new file mode 100644 index 000000000..d795d91a5 --- /dev/null +++ b/tests/etcdv3/postgres_utils.go @@ -0,0 +1,56 @@ +package etcdv3_test + +import ( + "context" + "fmt" + "maps" + "strings" + "time" + + "github.com/jackc/pgx/v5" +) + +type pgConnParams map[string]string + +func (cp pgConnParams) connString() string { + var connString []string + for k, v := range cp { + v = fmt.Sprintf("'%s'", strings.ReplaceAll(v, "'", `\'`)) + item := fmt.Sprintf("%s=%s", k, v) + connString = append(connString, item) + } + return strings.Join(connString, " ") +} + +func (cp pgConnParams) setParam(key, value string) pgConnParams { + newCp := maps.Clone(cp) + newCp[key] = value + return newCp +} + +func pgPing(ctx context.Context, connParams pgConnParams) error { + conn, err := pgx.Connect(ctx, connParams.connString()) + if err != nil { + return err + } + defer conn.Close(ctx) + + var name string + err = conn.QueryRow(ctx, "select datname from pg_database;").Scan(&name) + if err != nil { + return err + } + return nil +} + +func isReady(ctx context.Context, connParams pgConnParams, interval time.Duration) error { + for { + if err := ctx.Err(); err != nil { + return err + } + if err := pgPing(ctx, connParams); err != nil { + return nil + } + time.Sleep(interval) + } +} diff --git a/tests/etcdv3/smoke_test.go b/tests/etcdv3/smoke_test.go new file mode 100644 index 000000000..c2b9bbbe0 --- /dev/null +++ b/tests/etcdv3/smoke_test.go @@ -0,0 +1,169 @@ +// Package etcdv3_test will run integration tests for using etcd as backend with v3 api +package etcdv3_test + +import ( + "context" + "fmt" + "maps" + "os" + "time" + + "github.com/docker/go-connections/nat" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/etcd" + "github.com/testcontainers/testcontainers-go/network" +) + +var _ = Describe("Smoke", Ordered, func() { + const ( + numEtcd = 1 + autoRemove = false + initialConfig = `{"stolon_custom_config":{"defaultSUReplAccessMode":"strict","pgParameters":{},"pgHBA":[]}}` + + numKeepers = 3 + + pgPassword = "test123" + ) + var ( + ctx context.Context + nw *testcontainers.DockerNetwork + etcdContainer *etcd.EtcdContainer + etcdEndpoints string + sentinelCnt testcontainers.Container + proxyCnt testcontainers.Container + keeperContainers []testcontainers.Container + allContainers []testcontainers.Container + keeperSettings = map[string]string{ + "pg-repl-password": pgPassword, + "pg-su-password": pgPassword, + } + pgConn = pgConnParams{ + "host": "localhost", + "user": pgUser, + "password": pgPassword, + "dbname": pgDatabase, + } + ) + + BeforeAll(func() { + // RYUK requires permissions we don't need and don't want to implement + os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + + ctx = context.Background() + + var nwErr error + nw, nwErr = network.New(ctx) + Ω(nwErr).NotTo(HaveOccurred()) + + // setup etcd + var etcdErr error + etcdContainer, etcdEndpoints, etcdErr = runEtcd(ctx, etcdImage, nw) + Ω(etcdErr).NotTo(HaveOccurred()) + allContainers = []testcontainers.Container{etcdContainer} + + // run stolonctl to define a new cluster in etcd + stolonCtlInitCnt, initErr := runStolonCtl( + ctx, + etcdEndpoints, + nw, + "init", "--yes") + Ω(initErr).NotTo(HaveOccurred()) + allContainers = append(allContainers, stolonCtlInitCnt) + + // Run stolonctl patch to set initial config + stolonCtlPatchCnt, patchErr := runStolonCtl( + ctx, + etcdEndpoints, + nw, + "update", + "--patch", + initialConfig, + ) + Ω(patchErr).NotTo(HaveOccurred()) + allContainers = append(allContainers, stolonCtlPatchCnt) + // - cat myspec.json | stolonctl update --patch --file - + // or + // - stolonctl update --patch "${MYSPEC}" + // or + // - stolonctl update --patch --file "${STOLONCTL_FILE}"' + + // Start sentinel + var sentinelErr error + sentinelCnt, sentinelErr = runSentinel(ctx, etcdEndpoints, nw) + Ω(sentinelErr).NotTo(HaveOccurred()) + allContainers = append(allContainers, sentinelCnt) + + // start keeper(s) + for i := 0; i < numKeepers; i++ { + alias := fmt.Sprintf("keeper_%d", i) + settings := maps.Clone(keeperSettings) + settings["pg-listen-address"] = alias + aliases := map[string][]string{} + aliases[nw.Name] = []string{alias} + cnt, keeperErr := runKeeper(ctx, etcdEndpoints, nw, aliases, settings) + Ω(keeperErr).NotTo(HaveOccurred()) + keeperContainers = append(keeperContainers, cnt) + allContainers = append(allContainers, cnt) + } + + // Start proxy + var proxyErr error + aliases := map[string][]string{} + aliases[nw.Name] = []string{"proxy"} + proxyCnt, proxyErr = runProxy(ctx, etcdEndpoints, nw, aliases) + Ω(proxyErr).NotTo(HaveOccurred()) + allContainers = append(allContainers, proxyCnt) + + /* + logs, logErr := cnt.Logs(ctx) + Ω(logErr).NotTo(HaveOccurred()) + data, readErr := io.ReadAll(logs) + Ω(readErr).NotTo(HaveOccurred()) + fmt.Fprintf(GinkgoWriter, "DEBUG - Logs: %s", string(data)) + */ + // wait for postgres to be available + }) + AfterAll(func() { + if !autoRemove { + return + } + if CurrentSpecReport().Failed() { + GinkgoWriter.Printf("Test failed! not cleaning containers") + return + } + for _, cnt := range allContainers { + Ω(cnt.Terminate(ctx)).NotTo(HaveOccurred()) + } + Ω(nw.Remove(ctx)).NotTo(HaveOccurred()) + }) + Context("when connecting to the keepers", func() { + It("should work properly", func() { + for _, cnt := range keeperContainers { + natPort, err := cnt.MappedPort(ctx, + nat.Port(fmt.Sprintf("%d/tcp", keeperPort))) + Ω(err).NotTo(HaveOccurred()) + Ω(pgPing( + ctx, + pgConn.setParam("port", natPort.Port())), + ).NotTo(HaveOccurred()) + } + }) + }) + Context("when connecting through proxy", func() { + It("should work properly", func() { + natPort, err := proxyCnt.MappedPort(ctx, + nat.Port(fmt.Sprintf("%d/tcp", proxyPort))) + Ω(err).NotTo(HaveOccurred()) + proxyConnSettings := pgConn.setParam("port", natPort.Port()) + // This does not work directly after starting the container but does after 5s. + // So, we will try this for 10 seconds + isReadyCtx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(time.Second*10)) + defer cancelFunc() + // every 100 miliseconds + isReadyErr := isReady(isReadyCtx, proxyConnSettings, time.Millisecond*100) + Ω(isReadyErr).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/tests/etcdv3/suite_test.go b/tests/etcdv3/suite_test.go new file mode 100644 index 000000000..9234cc2c8 --- /dev/null +++ b/tests/etcdv3/suite_test.go @@ -0,0 +1,13 @@ +package etcdv3_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestEtcdv2(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Etcdv3 Suite") +} diff --git a/tests/integration/config_test.go b/tests/integration/config_test.go index b9fff05b2..bb5629142 100644 --- a/tests/integration/config_test.go +++ b/tests/integration/config_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,20 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package integration holds all integration tests package integration import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "testing" "time" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/gofrs/uuid" ) @@ -33,13 +35,13 @@ import ( func TestServerParameters(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -58,8 +60,8 @@ func TestServerParameters(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -68,14 +70,14 @@ func TestServerParameters(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -83,14 +85,14 @@ func TestServerParameters(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "unexistent_parameter": "value" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "unexistent_parameter": "value" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -117,7 +119,7 @@ func TestServerParameters(t *testing.T) { } // Fix wrong parameters - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : null }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : null }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -130,13 +132,13 @@ func TestServerParameters(t *testing.T) { func TestWalLevel(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -155,8 +157,8 @@ func TestWalLevel(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -165,14 +167,14 @@ func TestWalLevel(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -180,7 +182,7 @@ func TestWalLevel(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { @@ -188,7 +190,7 @@ func TestWalLevel(t *testing.T) { } // "archive" isn't an accepted wal_level - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "archive" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "archive" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -215,7 +217,7 @@ func TestWalLevel(t *testing.T) { } // "logical" is an accepted wal_level - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "logical" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "logical" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -245,13 +247,13 @@ func TestWalLevel(t *testing.T) { func TestWalKeepSegments(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -270,8 +272,8 @@ func TestWalKeepSegments(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -280,14 +282,14 @@ func TestWalKeepSegments(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -295,23 +297,23 @@ func TestWalKeepSegments(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - maj, _, err := tk.PGDataVersion() + version, err := tk.PGDataVersion() if err != nil { t.Fatalf("unexpected err: %v", err) } - if maj >= 13 { - t.Skipf("skipping since postgres version %d >= 13", maj) + if version.GreaterThanEqual(pg.V13) { + t.Skipf("skipping since postgres version %s >= 13", version) } // "archive" isn't an accepted wal_level - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "archive" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "archive" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -338,7 +340,7 @@ func TestWalKeepSegments(t *testing.T) { } // test setting a wal_keep_segments value greater than the default - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "20" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "20" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -365,7 +367,7 @@ func TestWalKeepSegments(t *testing.T) { } // test setting a wal_keep_segments value less than the default - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "5" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "5" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -392,7 +394,7 @@ func TestWalKeepSegments(t *testing.T) { } // test setting a bad wal_keep_segments value - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "badvalue" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_keep_segments": "badvalue" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -418,13 +420,13 @@ func TestWalKeepSegments(t *testing.T) { func TestAlterSystem(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -443,8 +445,8 @@ func TestAlterSystem(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -453,14 +455,14 @@ func TestAlterSystem(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -468,7 +470,7 @@ func TestAlterSystem(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { @@ -503,7 +505,7 @@ func TestAlterSystem(t *testing.T) { func TestAdditionalReplicationSlots(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -551,7 +553,7 @@ func TestAdditionalReplicationSlots(t *testing.T) { } // create additional replslots on master - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01", "replslot02" ] }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01", "replslot02" ] }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -564,7 +566,7 @@ func TestAdditionalReplicationSlots(t *testing.T) { } // remove replslot02 - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01" ] }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01" ] }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -577,7 +579,7 @@ func TestAdditionalReplicationSlots(t *testing.T) { } // remove additional replslots on master - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : null }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : null }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -590,7 +592,7 @@ func TestAdditionalReplicationSlots(t *testing.T) { } // create additional replslots on master - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01", "replslot02" ] }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "additionalMasterReplicationSlots" : [ "replslot01", "replslot02" ] }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -623,10 +625,10 @@ func TestAdditionalReplicationSlots(t *testing.T) { master.Stop() // Wait for cluster data containing standby as master - if err := WaitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standby.uid) } - if err := standby.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := standby.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -639,13 +641,13 @@ func TestAdditionalReplicationSlots(t *testing.T) { func TestAutomaticPgRestart(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -666,8 +668,8 @@ func TestAutomaticPgRestart(t *testing.T) { automaticPgRestart := true pgParameters := map[string]string{"max_connections": "100"} - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, AutomaticPgRestart: &automaticPgRestart, PGParameters: pgParameters, } @@ -676,7 +678,7 @@ func TestAutomaticPgRestart(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -685,7 +687,7 @@ func TestAutomaticPgRestart(t *testing.T) { } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -694,14 +696,14 @@ func TestAutomaticPgRestart(t *testing.T) { } defer tk.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "150" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "150" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -728,12 +730,12 @@ func TestAutomaticPgRestart(t *testing.T) { } // Allow users to opt out - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "automaticPgRestart" : false }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "automaticPgRestart" : false }`) if err != nil { t.Fatalf("unexpected err: %v", err) } - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "200" } }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "200" } }`) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -763,13 +765,13 @@ func TestAutomaticPgRestart(t *testing.T) { func TestAdvertise(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -788,8 +790,8 @@ func TestAdvertise(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -798,7 +800,7 @@ func TestAdvertise(t *testing.T) { if err != nil { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -809,7 +811,7 @@ func TestAdvertise(t *testing.T) { // Start keeper with advertise config advertiseConfig := []string{"--pg-advertise-address=6.6.6.6", "--pg-advertise-port=6666"} - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints, advertiseConfig...) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints, advertiseConfig...) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -818,7 +820,7 @@ func TestAdvertise(t *testing.T) { } defer tk.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { @@ -845,3 +847,82 @@ func TestAdvertise(t *testing.T) { } } } + +func TestKeeperBootsWithWalDir(t *testing.T) { + var uid uuid.UUID + t.Parallel() + + dir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + tstore, err := newTestStore(t, dir) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tstore.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tstore.WaitUp(10 * time.Second); err != nil { + t.Fatalf("error waiting on store up: %v", err) + } + storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port) + defer tstore.Stop() + if uid, err = uuid.NewV4(); err != nil { + t.Fatalf("error getting new UUD: %v", err) + } + clusterName := uid.String() + + storePath := filepath.Join(common.StorePrefix, clusterName) + + sm := store.NewKVBackedStore(tstore.store, storePath) + automaticPgRestart := true + pgParameters := map[string]string{"max_connections": "100"} + + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, + AutomaticPgRestart: &automaticPgRestart, + PGParameters: pgParameters, + } + + initialClusterSpecFile, err := writeClusterSpec(dir, initialClusterSpec) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := ts.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer ts.Stop() + + waldir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints, "--wal-dir", waldir) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer tk.Stop() + + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk.WaitDBUp(60 * time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Test that the Keeper is accepting queries + if _, err := tk.Exec("select now()"); err != nil { + t.Fatalf("unexpected err: %v", err) + } +} diff --git a/tests/integration/ha_test.go b/tests/integration/ha_test.go index b60ec7c65..061cdf041 100644 --- a/tests/integration/ha_test.go +++ b/tests/integration/ha_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +19,6 @@ import ( "context" "database/sql" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -27,10 +27,11 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - pg "github.com/sorintlab/stolon/internal/postgresql" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" ) const ( @@ -40,11 +41,11 @@ const ( pgSUPassword = "stolon_superuserpassword" ) -type testKeepers map[string]*TestKeeper -type testSentinels map[string]*TestSentinel +type testKeepers map[string]*testKeeper +type testSentinels map[string]*testSentinel -func setupStore(t *testing.T, dir string) *TestStore { - tstore, err := NewTestStore(t, dir) +func setupStore(t *testing.T, dir string) *testStore { + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -60,7 +61,7 @@ func setupStore(t *testing.T, dir string) *TestStore { func TestInitWithMultipleKeepers(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -76,8 +77,8 @@ func TestInitWithMultipleKeepers(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, FailInterval: &cluster.Duration{Duration: 10 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, } @@ -91,7 +92,7 @@ func TestInitWithMultipleKeepers(t *testing.T) { // Start 3 keepers for i := uint8(0); i < 3; i++ { - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -103,7 +104,7 @@ func TestInitWithMultipleKeepers(t *testing.T) { // Start 2 sentinels for i := uint8(0); i < 2; i++ { - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -116,7 +117,7 @@ func TestInitWithMultipleKeepers(t *testing.T) { defer shutdown(tks, tss, nil, tstore) // Wait for clusterView containing a master - masterUID, err := WaitClusterDataWithMaster(sm, 60*time.Second) + masterUID, err := waitClusterDataWithMaster(sm, 60*time.Second) if err != nil { t.Fatal("expected a master in cluster view") } @@ -126,35 +127,43 @@ func TestInitWithMultipleKeepers(t *testing.T) { // optionSetter implements the "functional options" pattern for ClusterSpec. // Useful to avoid modifying all the tests that depend on 'setupServers' signature, // whenever a new options is needed. -type optionSetter func(*cluster.ClusterSpec) +type optionSetter func(*cluster.Spec) // WithMinSync0 == true sets the MinimumSynchronousStandbys to 0 in ClusterSpec func withMinSync0(minSync0 bool) optionSetter { - return func(s *cluster.ClusterSpec) { + return func(s *cluster.Spec) { if minSync0 { - s.MinSynchronousStandbys = cluster.Uint16P(0) + s.MinSynchronousStandbys = util.ToPtr(uint16(0)) } } } -func setupServers(t *testing.T, clusterName, dir string, numKeepers, numSentinels uint8, syncRepl bool, usePgrewind bool, primaryKeeper *TestKeeper, otherOptions ...optionSetter) (testKeepers, testSentinels, *TestProxy, *TestStore) { - var initialClusterSpec *cluster.ClusterSpec +func setupServers( + t *testing.T, + clusterName, dir string, + numKeepers, numSentinels uint8, + syncRepl bool, + usePgrewind bool, + primaryKeeper *testKeeper, + otherOptions ...optionSetter, +) (testKeepers, testSentinels, *testProxy, *testStore) { + var initialClusterSpec *cluster.Spec if primaryKeeper == nil { - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec = &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbyLag: cluster.Uint32P(50 * 1024), // limit lag to 50kiB - SynchronousReplication: cluster.BoolP(syncRepl), - UsePgrewind: cluster.BoolP(usePgrewind), + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB + SynchronousReplication: util.ToPtr(syncRepl), + UsePgrewind: util.ToPtr(usePgrewind), PGParameters: defaultPGParameters, } } else { // if primaryKeeper is provided then we should create a standby cluster and do a // pitr recovery from the external primary database - pgpass, err := ioutil.TempFile(dir, "pgpass") + pgpass, err := os.CreateTemp(dir, "pgpass") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -163,21 +172,32 @@ func setupServers(t *testing.T, clusterName, dir string, numKeepers, numSentinel } pgpass.Close() - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModePITR), - Role: cluster.ClusterRoleP(cluster.ClusterRoleStandby), + initialClusterSpec = &cluster.Spec{ + InitMode: &pitrCluster, + Role: &replica, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbyLag: cluster.Uint32P(50 * 1024), // limit lag to 50kiB - SynchronousReplication: cluster.BoolP(syncRepl), + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB + SynchronousReplication: util.ToPtr(syncRepl), PGParameters: defaultPGParameters, PITRConfig: &cluster.PITRConfig{ - DataRestoreCommand: fmt.Sprintf("PGPASSFILE=%s pg_basebackup -D %%d -h %s -p %s -U %s", pgpass.Name(), primaryKeeper.pgListenAddress, primaryKeeper.pgPort, primaryKeeper.pgReplUsername), + DataRestoreCommand: fmt.Sprintf( + "PGPASSFILE=%s pg_basebackup -D %%d -h %s -p %s -U %s", + pgpass.Name(), + primaryKeeper.pgListenAddress, + primaryKeeper.pgPort, + primaryKeeper.pgReplUsername, + ), }, StandbyConfig: &cluster.StandbyConfig{ StandbySettings: &cluster.StandbySettings{ - PrimaryConninfo: fmt.Sprintf("sslmode=disable host=%s port=%s user=%s password=%s", primaryKeeper.pgListenAddress, primaryKeeper.pgPort, primaryKeeper.pgReplUsername, primaryKeeper.pgReplPassword), + PrimaryConninfo: fmt.Sprintf("sslmode=disable host=%s port=%s user=%s password=%s", + primaryKeeper.pgListenAddress, + primaryKeeper.pgPort, + primaryKeeper.pgReplUsername, + primaryKeeper.pgReplPassword, + ), }, }, } @@ -189,7 +209,7 @@ func setupServers(t *testing.T, clusterName, dir string, numKeepers, numSentinel return setupServersCustom(t, clusterName, dir, numKeepers, numSentinels, initialClusterSpec) } -func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSentinels uint8, initialClusterSpec *cluster.ClusterSpec) (testKeepers, testSentinels, *TestProxy, *TestStore) { +func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSentinels uint8, initialClusterSpec *cluster.Spec) (testKeepers, testSentinels, *testProxy, *testStore) { tstore := setupStore(t, dir) storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port) @@ -199,12 +219,12 @@ func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSe t.Fatalf("unexpected err: %v", err) } - tks := map[string]*TestKeeper{} - tss := map[string]*TestSentinel{} + tks := map[string]*testKeeper{} + tss := map[string]*testSentinel{} // Start sentinels for i := uint8(0); i < numSentinels; i++ { - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -216,7 +236,7 @@ func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSe // Start other keepers for i := uint8(0); i < numKeepers; i++ { - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -226,7 +246,7 @@ func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSe tks[tk.uid] = tk } - tp, err := NewTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tp, err := newTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -236,17 +256,17 @@ func setupServersCustom(t *testing.T, clusterName, dir string, numKeepers, numSe return tks, tss, tp, tstore } -func populate(t *testing.T, tk *TestKeeper) error { +func populate(_ *testing.T, tk *testKeeper) error { _, err := tk.Exec("CREATE TABLE table01(ID INT PRIMARY KEY NOT NULL, VALUE INT NOT NULL)") return err } -func write(t *testing.T, tk *TestKeeper, id, value int) error { +func write(_ *testing.T, tk *testKeeper, id, value int) error { _, err := tk.Exec("INSERT INTO table01 VALUES ($1, $2)", id, value) return err } -func getLines(t *testing.T, q Querier) (int, error) { +func getLines(_ *testing.T, q querier) (int, error) { rows, err := q.Query("SELECT FROM table01") if err != nil { return 0, err @@ -258,7 +278,7 @@ func getLines(t *testing.T, q Querier) (int, error) { return c, rows.Err() } -func waitLines(t *testing.T, q Querier, num int, timeout time.Duration) error { +func waitLines(t *testing.T, q querier, num int, timeout time.Duration) error { start := time.Now() c := -1 for time.Now().Add(-timeout).Before(start) { @@ -275,7 +295,7 @@ func waitLines(t *testing.T, q Querier, num int, timeout time.Duration) error { return fmt.Errorf("timeout waiting for %d lines, got: %d", num, c) } -func shutdown(tks map[string]*TestKeeper, tss map[string]*TestSentinel, tp *TestProxy, tstore *TestStore) { +func shutdown(tks map[string]*testKeeper, tss map[string]*testSentinel, tp *testProxy, tstore *testStore) { for _, ts := range tss { if ts.cmd != nil { ts.Stop() @@ -296,8 +316,8 @@ func shutdown(tks map[string]*TestKeeper, tss map[string]*TestSentinel, tp *Test } } -func waitKeeperReady(t *testing.T, sm *store.KVBackedStore, keeper *TestKeeper) { - if err := WaitClusterDataKeeperInitialized(keeper.uid, sm, 60*time.Second); err != nil { +func waitKeeperReady(t *testing.T, sm *store.KVBackedStore, keeper *testKeeper) { + if err := waitClusterDataKeeperInitialized(keeper.uid, sm, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := keeper.WaitDBUp(60 * time.Second); err != nil { @@ -305,9 +325,9 @@ func waitKeeperReady(t *testing.T, sm *store.KVBackedStore, keeper *TestKeeper) } } -func waitMasterStandbysReady(t *testing.T, sm *store.KVBackedStore, tks testKeepers) (master *TestKeeper, standbys []*TestKeeper) { +func waitMasterStandbysReady(t *testing.T, sm *store.KVBackedStore, tks testKeepers) (master *testKeeper, standbys []*testKeeper) { // Wait for normal cluster phase (master ready) - masterUID, err := WaitClusterDataWithMaster(sm, 60*time.Second) + masterUID, err := waitClusterDataWithMaster(sm, 60*time.Second) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -323,11 +343,11 @@ func waitMasterStandbysReady(t *testing.T, sm *store.KVBackedStore, tks testKeep for _, standby := range standbys { waitKeeperReady(t, sm, standby) } - return + return master, standbys } func testMasterStandby(t *testing.T, syncRepl bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -379,14 +399,14 @@ func TestMasterStandbySyncRepl(t *testing.T) { } func testFailover(t *testing.T, syncRepl bool, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -427,12 +447,12 @@ func testFailover(t *testing.T, syncRepl bool, standbyCluster bool) { } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -446,10 +466,10 @@ func testFailover(t *testing.T, syncRepl bool, standbyCluster bool) { master.Stop() // Wait for cluster data containing standby as master - if err := WaitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standby.uid) } - if err := standby.WaitDBRole(common.RoleMaster, ptk, 30*time.Second); err != nil { + if err := standby.WaitDBRole(common.RolePrimary, ptk, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -490,14 +510,14 @@ func TestFailoverSyncReplStandbyCluster(t *testing.T) { // Tests standby elected as new master but fails to become master. Then old // master comes back and is re-elected as master. func testFailoverFailed(t *testing.T, syncRepl bool, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -538,12 +558,12 @@ func testFailoverFailed(t *testing.T, syncRepl bool, standbyCluster bool) { } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -552,7 +572,7 @@ func testFailoverFailed(t *testing.T, syncRepl bool, standbyCluster bool) { master.Stop() // Wait for cluster data containing standby as master - if err := WaitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standby.uid) } @@ -567,7 +587,7 @@ func testFailoverFailed(t *testing.T, syncRepl bool, standbyCluster bool) { t.Fatalf("unexpected err: %v", err) } // Wait for cluster data containing previous master as master - err = WaitClusterDataMaster(master.uid, sm, 30*time.Second) + err = waitClusterDataMaster(master.uid, sm, 30*time.Second) if !syncRepl && err != nil { t.Fatalf("expected master %q in cluster view", master.uid) } @@ -602,14 +622,14 @@ func TestFailoverFailedSyncStandbyCluster(t *testing.T) { // master (reported) xlogpos won't be elected as the new master. This test is // valid only for asynchronous replication func testFailoverTooMuchLag(t *testing.T, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -665,7 +685,7 @@ func testFailoverTooMuchLag(t *testing.T, standbyCluster bool) { } // standby shouldn't be elected as master since its lag is greater than MaxStandbyLag - if err := standby.WaitDBRole(common.RoleMaster, ptk, 30*time.Second); err == nil { + if err := standby.WaitDBRole(common.RolePrimary, ptk, 30*time.Second); err == nil { t.Fatalf("standby shouldn't be elected as master") } } @@ -681,14 +701,14 @@ func TestFailoverTooMuchLagStandbyCluster(t *testing.T) { } func testOldMasterRestart(t *testing.T, syncRepl, minSync0 bool, usePgrewind bool, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -731,12 +751,12 @@ func testOldMasterRestart(t *testing.T, syncRepl, minSync0 bool, usePgrewind boo } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -744,7 +764,7 @@ func testOldMasterRestart(t *testing.T, syncRepl, minSync0 bool, usePgrewind boo t.Logf("Stopping current master keeper: %s", master.uid) master.Stop() - if err := standbys[0].WaitDBRole(common.RoleMaster, ptk, 30*time.Second); err != nil { + if err := standbys[0].WaitDBRole(common.RolePrimary, ptk, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -761,7 +781,7 @@ func testOldMasterRestart(t *testing.T, syncRepl, minSync0 bool, usePgrewind boo // writing to the new master since there's not active synchronous // standby. if !minSync0 { - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -794,7 +814,7 @@ func testOldMasterRestart(t *testing.T, syncRepl, minSync0 bool, usePgrewind boo if err := waitLines(t, master, 2, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := master.WaitDBRole(common.RoleStandby, ptk, 30*time.Second); err != nil { + if err := master.WaitDBRole(common.RoleReplica, ptk, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } } @@ -835,14 +855,14 @@ func TestOldMasterRestartStandbyCluster(t *testing.T) { } func testPartition1(t *testing.T, syncRepl, minSync0, usePgrewind bool, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -885,12 +905,12 @@ func testPartition1(t *testing.T, syncRepl, minSync0, usePgrewind bool, standbyC } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -903,7 +923,7 @@ func testPartition1(t *testing.T, syncRepl, minSync0, usePgrewind bool, standbyC if err := master.SignalPG(syscall.SIGSTOP); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := standbys[0].WaitDBRole(common.RoleMaster, ptk, 60*time.Second); err != nil { + if err := standbys[0].WaitDBRole(common.RolePrimary, ptk, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -920,7 +940,7 @@ func testPartition1(t *testing.T, syncRepl, minSync0, usePgrewind bool, standbyC // the test will block forever when writing to the new master since // there's not active synchronous standby. if !minSync0 { - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -963,7 +983,7 @@ func testPartition1(t *testing.T, syncRepl, minSync0, usePgrewind bool, standbyC t.Fatalf("unexpected err: %v", err) } // Old master should become a standby of the new one - if err := master.WaitDBRole(common.RoleStandby, ptk, 60*time.Second); err != nil { + if err := master.WaitDBRole(common.RoleReplica, ptk, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } } @@ -1004,7 +1024,7 @@ func TestPartition1StandbyCluster(t *testing.T) { } func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1040,7 +1060,7 @@ func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { } // Add another standby - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1057,12 +1077,12 @@ func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(master) + xLogPos, err := getXLogPos(master) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for standby[0] to have reported its state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1084,12 +1104,12 @@ func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { } // get the primary/master XLogPos - xLogPos, err = GetXLogPos(master) + xLogPos, err = getXLogPos(master) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for standby[1] to have reported its state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standbys[1]}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standbys[1]}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1117,7 +1137,7 @@ func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { // Wait for cluster data waitKeeperReady(t, sm, standbys[0]) - err = standbys[0].WaitDBRole(common.RoleMaster, nil, 60*time.Second) + err = standbys[0].WaitDBRole(common.RolePrimary, nil, 60*time.Second) if !syncRepl && err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1160,7 +1180,7 @@ func testTimelineFork(t *testing.T, syncRepl, usePgrewind bool) { if err := waitLines(t, standbys[1], 3, 120*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := standbys[1].WaitDBRole(common.RoleStandby, nil, 60*time.Second); err != nil { + if err := standbys[1].WaitDBRole(common.RoleReplica, nil, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1197,14 +1217,14 @@ func TestTimelineForkSyncReplPgrewind(t *testing.T) { // postgres (without triggering failover since it restart before being marked // ad failed) make the slave continue to sync using the new address func testMasterChangedAddress(t *testing.T, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -1237,12 +1257,12 @@ func testMasterChangedAddress(t *testing.T, standbyCluster bool) { } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standbys[0]}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1256,7 +1276,7 @@ func testMasterChangedAddress(t *testing.T, standbyCluster bool) { t.Logf("Restarting current master keeper %q with different addresses", master.uid) master.Stop() storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port) - master, err = NewTestKeeperWithID(t, dir, master.uid, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + master, err = newTestKeeperWithID(t, dir, master.uid, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1270,7 +1290,7 @@ func testMasterChangedAddress(t *testing.T, standbyCluster bool) { t.Fatalf("unexpected err: %v", err) } - if err := master.WaitDBRole(common.RoleMaster, ptk, 30*time.Second); err != nil { + if err := master.WaitDBRole(common.RolePrimary, ptk, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1295,7 +1315,7 @@ func TestMasterChangedAddressStandbyCluster(t *testing.T) { func TestFailedStandby(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1303,12 +1323,12 @@ func TestFailedStandby(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), PGParameters: defaultPGParameters, } @@ -1320,7 +1340,7 @@ func TestFailedStandby(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) // Wait for clusterView containing a master - masterUID, err := WaitClusterDataWithMaster(sm, 30*time.Second) + masterUID, err := waitClusterDataWithMaster(sm, 30*time.Second) if err != nil { t.Fatal("expected a master in cluster view") } @@ -1341,7 +1361,7 @@ func TestFailedStandby(t *testing.T) { t.Fatalf("wrong number of lines, want: %d, got: %d", 1, c) } - if err := WaitNumDBs(sm, 2, 30*time.Second); err != nil { + if err := waitNumDBs(sm, 2, 30*time.Second); err != nil { t.Fatalf("expected 2 DBs in cluster data: %v", err) } @@ -1351,7 +1371,7 @@ func TestFailedStandby(t *testing.T) { } // Get current standby - var standby *TestKeeper + var standby *testKeeper for _, db := range cd.DBs { if db.UID == cd.Cluster.Status.Master { continue @@ -1367,26 +1387,26 @@ func TestFailedStandby(t *testing.T) { standby.Stop() // Wait for other keeper to have a standby db assigned - var newStandby *TestKeeper + var newStandby *testKeeper for _, tk := range tks { if tk.uid != master.uid && tk.uid != standby.uid { newStandby = tk } } - if err := WaitStandbyKeeper(sm, newStandby.uid, 30*time.Second); err != nil { + if err := waitStandbyKeeper(sm, newStandby.uid, 30*time.Second); err != nil { t.Fatalf("expected keeper %s to have a standby db assigned: %v", newStandby.uid, err) } // Wait for new standby declared as good and remove of old standby - if err := WaitNumDBs(sm, 2, 30*time.Second); err != nil { + if err := waitNumDBs(sm, 2, 30*time.Second); err != nil { t.Fatalf("expected 2 DBs in cluster data: %v", err) } } func TestLoweredMaxStandbysPerSender(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1394,12 +1414,12 @@ func TestLoweredMaxStandbysPerSender(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbysPerSender: cluster.Uint16P(2), + MaxStandbysPerSender: util.ToPtr(uint16(2)), PGParameters: defaultPGParameters, } @@ -1412,7 +1432,7 @@ func TestLoweredMaxStandbysPerSender(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) // Wait for clusterView containing a master - masterUID, err := WaitClusterDataWithMaster(sm, 30*time.Second) + masterUID, err := waitClusterDataWithMaster(sm, 30*time.Second) if err != nil { t.Fatal("expected a master in cluster view") } @@ -1433,25 +1453,25 @@ func TestLoweredMaxStandbysPerSender(t *testing.T) { t.Fatalf("wrong number of lines, want: %d, got: %d", 1, c) } - if err := WaitNumDBs(sm, 3, 30*time.Second); err != nil { + if err := waitNumDBs(sm, 3, 30*time.Second); err != nil { t.Fatalf("expected 3 DBs in cluster data: %v", err) } // Set MaxStandbysPerSender to 1 - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "maxStandbysPerSender" : 1 }`) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "maxStandbysPerSender" : 1 }`) if err != nil { t.Fatalf("unexpected err: %v", err) } // Wait for only 1 standby - if err := WaitNumDBs(sm, 2, 30*time.Second); err != nil { + if err := waitNumDBs(sm, 2, 30*time.Second); err != nil { t.Fatalf("expected 2 DBs in cluster data: %v", err) } } func TestKeeperRemoval(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1459,14 +1479,14 @@ func TestKeeperRemoval(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, // very low DeadKeeperRemovalInterval to test this behavior DeadKeeperRemovalInterval: &cluster.Duration{Duration: 10 * time.Second}, - MaxStandbysPerSender: cluster.Uint16P(1), + MaxStandbysPerSender: util.ToPtr(uint16(1)), PGParameters: defaultPGParameters, } @@ -1489,7 +1509,7 @@ func TestKeeperRemoval(t *testing.T) { } // Add another keeper that won't have a db assigned (since MaxStandbysPerSender == 1) - standby2, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + standby2, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1500,7 +1520,7 @@ func TestKeeperRemoval(t *testing.T) { } // wait for keeper to be added to the cluster data - if err := WaitClusterDataKeepers([]string{master.uid, standby1.uid, standby2.uid}, sm, 30*time.Second); err != nil { + if err := waitClusterDataKeepers([]string{master.uid, standby1.uid, standby2.uid}, sm, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1509,7 +1529,7 @@ func TestKeeperRemoval(t *testing.T) { standby2.Stop() // wait for standby2 keeper to be removed from the cluster data since it's dead a without an assigned db - if err := WaitClusterDataKeepers([]string{master.uid, standby1.uid}, sm, 30*time.Second); err != nil { + if err := waitClusterDataKeepers([]string{master.uid, standby1.uid}, sm, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1521,7 +1541,7 @@ func TestKeeperRemoval(t *testing.T) { // wait for a time greater than DeadKeeperRemovalInterval time.Sleep(20 * time.Second) // the master keeper shouldn't be removed from the cluster data - if err := WaitClusterDataKeepers([]string{master.uid, standby1.uid}, sm, 30*time.Second); err != nil { + if err := waitClusterDataKeepers([]string{master.uid, standby1.uid}, sm, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1548,18 +1568,18 @@ func TestKeeperRemoval(t *testing.T) { if err := waitLines(t, standby2, 2, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := standby2.WaitDBRole(common.RoleStandby, nil, 30*time.Second); err != nil { + if err := standby2.WaitDBRole(common.RoleReplica, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } // wait for standby1 keeper to be removed from the cluster data since it's dead a without an assigned db - if err := WaitClusterDataKeepers([]string{master.uid, standby2.uid}, sm, 30*time.Second); err != nil { + if err := waitClusterDataKeepers([]string{master.uid, standby2.uid}, sm, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } } func testKeeperRemovalStolonCtl(t *testing.T, syncRepl bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1567,13 +1587,13 @@ func testKeeperRemovalStolonCtl(t *testing.T, syncRepl bool) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - SynchronousReplication: cluster.BoolP(syncRepl), - MaxSynchronousStandbys: cluster.Uint16P(3), + SynchronousReplication: util.ToPtr(syncRepl), + MaxSynchronousStandbys: util.ToPtr(uint16(3)), PGParameters: defaultPGParameters, } @@ -1587,13 +1607,13 @@ func testKeeperRemovalStolonCtl(t *testing.T, syncRepl bool) { master, standbys := waitMasterStandbysReady(t, sm, tks) - maj, min, err := master.PGDataVersion() + version, err := master.PGDataVersion() if err != nil { t.Fatalf("unexpected err: %v", err) } // on postgresql <= 9.5 we can have only 1 synchronous standby if syncRepl { - if maj == 9 && min <= 5 { + if version.LessThan(pg.V96) { ok := false if err := WaitClusterDataSynchronousStandbys([]string{standbys[0].uid}, sm, 30*time.Second); err == nil { ok = true @@ -1631,7 +1651,7 @@ func testKeeperRemovalStolonCtl(t *testing.T, syncRepl bool) { } // remove master from the cluster data, must fail - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "removekeeper", master.uid) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "removekeeper", master.uid) if err == nil { t.Fatalf("expected err") } @@ -1641,7 +1661,7 @@ func testKeeperRemovalStolonCtl(t *testing.T, syncRepl bool) { standbys[0].Stop() // remove standby[0] from the cluster data - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "removekeeper", standbys[0].uid) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "removekeeper", standbys[0].uid) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1685,7 +1705,7 @@ func TestKeeperRemovalStolonCtlSyncRepl(t *testing.T) { func TestStandbyCantSync(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1693,8 +1713,8 @@ func TestStandbyCantSync(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -1765,10 +1785,10 @@ func TestStandbyCantSync(t *testing.T) { master.Stop() // Wait for cluster data containing standbys[1] as master - if err := WaitClusterDataMaster(standbys[1].uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standbys[1].uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standbys[1].uid) } - if err := standbys[1].WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := standbys[1].WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1815,7 +1835,7 @@ func TestStandbyCantSync(t *testing.T) { func TestDisappearedKeeperData(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1840,12 +1860,12 @@ func TestDisappearedKeeperData(t *testing.T) { } // get the master XLogPos - xLogPos, err := GetXLogPos(master) + xLogPos, err := getXLogPos(master) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1874,10 +1894,10 @@ func TestDisappearedKeeperData(t *testing.T) { // master shouldn't start its postgres instance and standby should be elected as new master // Wait for cluster data containing standby as master - if err := WaitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standby.uid) } - if err := standby.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := standby.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1896,14 +1916,14 @@ func TestDisappearedKeeperData(t *testing.T) { } func testForceFail(t *testing.T, syncRepl bool, standbyCluster bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - var ptk *TestKeeper - var primary *TestKeeper + var ptk *testKeeper + var primary *testKeeper if standbyCluster { primaryClusterName := uuid.Must(uuid.NewV4()).String() ptks, ptss, ptp, ptstore := setupServers(t, primaryClusterName, dir, 1, 1, false, false, nil) @@ -1945,12 +1965,12 @@ func testForceFail(t *testing.T, syncRepl bool, standbyCluster bool) { } // get the primary/master XLogPos - xLogPos, err := GetXLogPos(primary) + xLogPos, err := getXLogPos(primary) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -1960,16 +1980,16 @@ func testForceFail(t *testing.T, syncRepl bool, standbyCluster bool) { } // mark master as failed - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "failkeeper", master.uid) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "failkeeper", master.uid) if err != nil { t.Fatalf("unexpected err: %v", err) } // Wait for cluster data containing standby as master - if err := WaitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { + if err := waitClusterDataMaster(standby.uid, sm, 30*time.Second); err != nil { t.Fatalf("expected master %q in cluster view", standby.uid) } - if err := standby.WaitDBRole(common.RoleMaster, ptk, 30*time.Second); err != nil { + if err := standby.WaitDBRole(common.RolePrimary, ptk, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -2012,7 +2032,7 @@ func TestForceFailSyncReplStandbyCluster(t *testing.T) { // defined synchronous standbys are in sync. func testSyncStandbyNotInSync(t *testing.T, minSync0 bool) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -2063,12 +2083,12 @@ func testSyncStandbyNotInSync(t *testing.T, minSync0 bool) { t.Fatalf("unexpected err: %v", err) } // get the master XLogPos - xLogPos, err := GetXLogPos(master) + xLogPos, err := getXLogPos(master) if err != nil { t.Fatalf("unexpected err: %v", err) } // wait for the keepers to have reported their state - if err := WaitClusterSyncedXLogPos([]*TestKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, sm, 20*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } // the proxy should connect to the right master @@ -2143,3 +2163,184 @@ func TestSyncStandbyNotInSync(t *testing.T) { func TestSyncStandbyNotInSync0(t *testing.T) { testSyncStandbyNotInSync(t, true) } + +func TestFailoverWithCustomWalDir(t *testing.T) { + var uid uuid.UUID + dir, err := os.MkdirTemp("", "stolon") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + // Set up store + tstore := setupStore(t, dir) + storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port) + + if uid, err = uuid.NewV4(); err != nil { + t.Fatalf("error getting new UUD: %v", err) + } + clusterName := uid.String() + + syncRep := true + usePgRewind := true + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, + SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, + FailInterval: &cluster.Duration{Duration: 5 * time.Second}, + ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB + SynchronousReplication: &syncRep, + UsePgrewind: &usePgRewind, + PGParameters: defaultPGParameters, + } + initialClusterSpecFile, err := writeClusterSpec(dir, initialClusterSpec) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Set up sentinel + sentinel, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := sentinel.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Set up first keeper + keeper1waldir, err := os.MkdirTemp("", "stolon") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + tk1, err := newTestKeeper( + t, + dir, + clusterName, + pgSUUsername, + pgSUPassword, + pgReplUsername, + pgReplPassword, + tstore.storeBackend, + storeEndpoints, + "--wal-dir", + keeper1waldir, + ) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk1.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Set up second keeper + keeper2waldir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + tk2, err := newTestKeeper( + t, + dir, + clusterName, + pgSUUsername, + pgSUPassword, + pgReplUsername, + pgReplPassword, + tstore.storeBackend, + storeEndpoints, + "--wal-dir", + keeper2waldir, + ) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk2.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + storePath := filepath.Join(common.StorePrefix, clusterName) + store := store.NewKVBackedStore(tstore.store, storePath) + + // Wait for keepers to become ready + if err := waitClusterPhase(store, cluster.Normal, 60*time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk1.WaitDBUp(60 * time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := tk2.WaitDBUp(60 * time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + keepers := map[string]*testKeeper{tk1.uid: tk1, tk2.uid: tk2} + sentinels := map[string]*testSentinel{sentinel.uid: sentinel} + + // Set up proxy + proxy, err := newTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := proxy.Start(); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + defer shutdown(keepers, sentinels, proxy, tstore) + + master, standbys := waitMasterStandbysReady(t, store, keepers) + standby := standbys[0] + + fmt.Printf("master: %s\n", master.uid) + fmt.Printf("standby: %s\n", standby.uid) + + if err := WaitClusterDataSynchronousStandbys([]string{standby.uid}, store, 30*time.Second); err != nil { + t.Fatalf("expected synchronous standby on keeper %q in cluster data", standby.uid) + } + + if err := populate(t, master); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if err := write(t, master, 1, 1); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // get the master XLogPos + xLogPos, err := getXLogPos(master) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + // wait for the keepers to have reported their state + if err := WaitClusterSyncedXLogPos([]*testKeeper{master, standby}, xLogPos, store, 20*time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // the proxy should connect to the right master + if err := proxy.WaitRightMaster(master, 3*cluster.DefaultProxyCheckInterval); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + // Stop the keeper process on master, should also stop the database + t.Logf("Stopping current master keeper: %s", master.uid) + master.Stop() + + // Wait for cluster data containing standby as master + if err := waitClusterDataMaster(standby.uid, store, 30*time.Second); err != nil { + t.Fatalf("expected master %q in cluster view", standby.uid) + } + if err := standby.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { + t.Fatalf("unexpected err: %v", err) + } + + c, err := getLines(t, standby) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if c != 1 { + t.Fatalf("wrong number of lines, want: %d, got: %d", 1, c) + } + + // the proxy should connect to the right master + if err := proxy.WaitRightMaster(standby, 3*cluster.DefaultProxyCheckInterval); err != nil { + t.Fatalf("unexpected err: %v", err) + } +} diff --git a/tests/integration/init_test.go b/tests/integration/init_test.go index 1fcec908e..a496ef4d2 100644 --- a/tests/integration/init_test.go +++ b/tests/integration/init_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,22 +18,22 @@ package integration import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/gofrs/uuid" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" ) func TestInit(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -45,8 +46,8 @@ func TestInit(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -57,7 +58,7 @@ func TestInit(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -65,7 +66,7 @@ func TestInit(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -94,7 +95,7 @@ func TestInitNewNoMerge(t *testing.T) { func testInitNew(t *testing.T, merge bool) { clusterName := uuid.Must(uuid.NewV4()).String() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -108,8 +109,8 @@ func testInitNew(t *testing.T, merge bool) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, FailInterval: &cluster.Duration{Duration: 10 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, MergePgParameters: &merge, @@ -120,14 +121,14 @@ func testInitNew(t *testing.T, merge bool) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -136,7 +137,7 @@ func testInitNew(t *testing.T, merge bool) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -173,7 +174,7 @@ func TestInitExistingNoMerge(t *testing.T) { func testInitExisting(t *testing.T, merge bool) { clusterName := uuid.Must(uuid.NewV4()).String() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -187,8 +188,8 @@ func testInitExisting(t *testing.T, merge bool) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -201,14 +202,14 @@ func testInitExisting(t *testing.T, merge bool) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } if err := ts.Start(); err != nil { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -217,7 +218,7 @@ func testInitExisting(t *testing.T, merge bool) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -233,8 +234,8 @@ func testInitExisting(t *testing.T, merge bool) { } // Now initialize a new cluster with the existing keeper - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeExisting), + initialClusterSpec = &cluster.Spec{ + InitMode: &existingCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -251,15 +252,15 @@ func testInitExisting(t *testing.T, merge bool) { t.Logf("reinitializing cluster") // Initialize cluster with new spec - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "init", "-y", "-f", initialClusterSpecFile) + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "init", "-y", "-f", initialClusterSpecFile) if err != nil { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseInitializing, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Initializing, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := tk.WaitDBUp(60 * time.Second); err != nil { @@ -305,7 +306,7 @@ func testInitExisting(t *testing.T, merge bool) { func TestInitUsers(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -318,7 +319,7 @@ func TestInitUsers(t *testing.T) { // Test pg-repl-username == pg-su-username but password different clusterName := uuid.Must(uuid.NewV4()).String() - tk, err := NewTestKeeper(t, dir, clusterName, "user01", "password01", "user01", "password02", tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, "user01", "password01", "user01", "password02", tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -336,8 +337,8 @@ func TestInitUsers(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -348,7 +349,7 @@ func TestInitUsers(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -357,11 +358,11 @@ func TestInitUsers(t *testing.T) { } defer ts.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseInitializing, 30*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Initializing, 30*time.Second); err != nil { t.Fatal("expected cluster in initializing phase") } - tk2, err := NewTestKeeper(t, dir, clusterName, "user01", "password", "user01", "password", tstore.storeBackend, storeEndpoints) + tk2, err := newTestKeeper(t, dir, clusterName, "user01", "password", "user01", "password", tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -379,7 +380,7 @@ func TestInitUsers(t *testing.T) { sm = store.NewKVBackedStore(tstore.store, storePath) - ts2, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts2, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -388,11 +389,11 @@ func TestInitUsers(t *testing.T) { } defer ts2.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseInitializing, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Initializing, 60*time.Second); err != nil { t.Fatal("expected cluster in initializing phase") } - tk3, err := NewTestKeeper(t, dir, clusterName, "user01", "password", "user02", "password", tstore.storeBackend, storeEndpoints) + tk3, err := newTestKeeper(t, dir, clusterName, "user01", "password", "user02", "password", tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -411,7 +412,7 @@ func TestInitUsers(t *testing.T) { func TestInitialClusterSpec(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -427,12 +428,12 @@ func TestInitialClusterSpec(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - SynchronousReplication: cluster.BoolP(true), + SynchronousReplication: util.ToPtr(true), PGParameters: defaultPGParameters, } initialClusterSpecFile, err := writeClusterSpec(dir, initialClusterSpec) @@ -440,7 +441,7 @@ func TestInitialClusterSpec(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -449,7 +450,7 @@ func TestInitialClusterSpec(t *testing.T) { } defer ts.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseInitializing, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Initializing, 60*time.Second); err != nil { t.Fatal("expected cluster in initializing phase") } @@ -465,13 +466,13 @@ func TestInitialClusterSpec(t *testing.T) { func TestExclusiveLock(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -489,7 +490,7 @@ func TestExclusiveLock(t *testing.T) { u := uuid.Must(uuid.NewV4()) id := fmt.Sprintf("%x", u[:4]) - tk1, err := NewTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk1, err := newTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -503,7 +504,7 @@ func TestExclusiveLock(t *testing.T) { t.Fatalf("expecting keeper reporting that exclusive lock on data dir has been taken") } - tk2, err := NewTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk2, err := newTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -521,13 +522,13 @@ func TestExclusiveLock(t *testing.T) { func TestPasswordTrailingNewLine(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -548,7 +549,7 @@ func TestPasswordTrailingNewLine(t *testing.T) { pgSUPassword := "stolon_superuserpassword\n" pgReplPassword := "stolon_replpassword\n" - tk, err := NewTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -567,7 +568,7 @@ func TestPasswordTrailingNewLine(t *testing.T) { pgSUPassword = "stolon_superuserpassword\n" pgReplPassword = "\n" - tk, err = NewTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err = newTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -587,7 +588,7 @@ func TestPasswordTrailingNewLine(t *testing.T) { pgSUPassword = "\n" pgReplPassword = "stolon_replpassword\n" - tk, err = NewTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err = newTestKeeperWithID(t, dir, id, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } diff --git a/tests/integration/main.go b/tests/integration/main.go new file mode 100644 index 000000000..ace279d48 --- /dev/null +++ b/tests/integration/main.go @@ -0,0 +1,10 @@ +package integration + +import cluster "github.com/pgvillage-tools/stolon/api/v1" + +var ( + newCluster = cluster.New + pitrCluster = cluster.PITR + existingCluster = cluster.ExistingCluster + replica = cluster.Replica +) diff --git a/tests/integration/pitr_test.go b/tests/integration/pitr_test.go index 6854457a6..3a9b69741 100644 --- a/tests/integration/pitr_test.go +++ b/tests/integration/pitr_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2016 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +18,15 @@ package integration import ( "context" "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" "testing" "time" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" "github.com/gofrs/uuid" ) @@ -44,17 +44,17 @@ func TestPITRRecoveryTarget(t *testing.T) { } func testPITR(t *testing.T, recoveryTarget bool) { - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - baseBackupDir, err := ioutil.TempDir(dir, "basebackup") + baseBackupDir, err := os.MkdirTemp(dir, "basebackup") if err != nil { t.Fatalf("unexpected err: %v", err) } - archiveBackupDir, err := ioutil.TempDir(dir, "archivebackup") + archiveBackupDir, err := os.MkdirTemp(dir, "archivebackup") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -70,8 +70,8 @@ func testPITR(t *testing.T, recoveryTarget bool) { sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -85,7 +85,7 @@ func testPITR(t *testing.T, recoveryTarget bool) { t.Fatalf("unexpected err: %v", err) } - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -94,7 +94,7 @@ func testPITR(t *testing.T, recoveryTarget bool) { } defer tk.Stop() - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -103,14 +103,14 @@ func testPITR(t *testing.T, recoveryTarget bool) { } // Wait for clusterView containing a master - _, err = WaitClusterDataWithMaster(sm, 30*time.Second) + _, err = waitClusterDataWithMaster(sm, 30*time.Second) if err != nil { t.Fatal("expected a master in cluster view") } if err := tk.WaitDBUp(60 * time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := tk.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := tk.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } if err := populate(t, tk); err != nil { @@ -124,7 +124,7 @@ func testPITR(t *testing.T, recoveryTarget bool) { now := time.Now() // ioutil.Tempfile already creates files with 0600 permissions - pgpass, err := ioutil.TempFile("", "pgpass") + pgpass, err := os.CreateTemp("", "pgpass") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -156,8 +156,8 @@ func testPITR(t *testing.T, recoveryTarget bool) { } // Now initialize a new cluster with the existing keeper - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModePITR), + initialClusterSpec = &cluster.Spec{ + InitMode: &pitrCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -185,7 +185,7 @@ func testPITR(t *testing.T, recoveryTarget bool) { t.Fatalf("unexpected err: %v", err) } - ts, err = NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err = newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -194,17 +194,17 @@ func testPITR(t *testing.T, recoveryTarget bool) { } defer ts.Stop() - if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil { + if err := waitClusterPhase(sm, cluster.Normal, 60*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - _, err = WaitClusterDataWithMaster(sm, 30*time.Second) + _, err = waitClusterDataWithMaster(sm, 30*time.Second) if err != nil { t.Fatal("expected a master in cluster view") } if err := tk.WaitDBUp(60 * time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } - if err := tk.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := tk.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } diff --git a/tests/integration/proxy_test.go b/tests/integration/proxy_test.go index d759857eb..558d80615 100644 --- a/tests/integration/proxy_test.go +++ b/tests/integration/proxy_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +18,6 @@ package integration import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "syscall" @@ -25,15 +25,15 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" ) func TestProxyListening(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -41,13 +41,14 @@ func TestProxyListening(t *testing.T) { clusterName := uuid.Must(uuid.NewV4()).String() - tstore, err := NewTestStore(t, dir) + tstore, err := newTestStore(t, dir) if err != nil { t.Fatalf("unexpected err: %v", err) } storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port) - tp, err := NewTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tp, err := newTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, + pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -80,18 +81,18 @@ func TestProxyListening(t *testing.T) { sm := store.NewKVBackedStore(tstore.store, storePath) - cd := &cluster.ClusterData{ + cd := &cluster.Data{ FormatVersion: cluster.CurrentCDFormatVersion, Cluster: &cluster.Cluster{ UID: "01", Generation: 1, - Spec: &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + Spec: &cluster.Spec{ + InitMode: &newCluster, FailInterval: &cluster.Duration{Duration: 10 * time.Second}, }, Status: cluster.ClusterStatus{ CurrentGeneration: 1, - Phase: cluster.ClusterPhaseNormal, + Phase: cluster.Normal, Master: "01", }, }, @@ -111,7 +112,7 @@ func TestProxyListening(t *testing.T) { ChangeTime: time.Time{}, Spec: &cluster.DBSpec{ KeeperUID: "01", - Role: common.RoleMaster, + Role: common.RolePrimary, Followers: []string{"02"}, }, Status: cluster.DBStatus{ diff --git a/tests/integration/sentinel_test.go b/tests/integration/sentinel_test.go index ceecfd20f..108d57923 100644 --- a/tests/integration/sentinel_test.go +++ b/tests/integration/sentinel_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2017 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +18,6 @@ package integration import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "reflect" @@ -26,15 +26,15 @@ import ( "time" "github.com/gofrs/uuid" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" ) func TestSentinelEnabledProxies(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "") + dir, err := os.MkdirTemp("", "") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -50,8 +50,8 @@ func TestSentinelEnabledProxies(t *testing.T) { storePath := filepath.Join(common.StorePrefix, clusterName) sm := store.NewKVBackedStore(tstore.store, storePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -62,7 +62,7 @@ func TestSentinelEnabledProxies(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -70,7 +70,7 @@ func TestSentinelEnabledProxies(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -80,7 +80,7 @@ func TestSentinelEnabledProxies(t *testing.T) { } defer tk.Stop() - tp, err := NewTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tp, err := newTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -112,7 +112,7 @@ func TestSentinelEnabledProxies(t *testing.T) { // check that the sentinel has become leader (cluster data is changed since // it's updating keepers status) TODO(sgotti) find a better way to determine // if the sentinel is the leader and is updating the clusterdata - if err := WaitClusterDataUpdated(sm, 30*time.Second); err != nil { + if err := waitClusterDataUpdated(sm, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -129,7 +129,7 @@ func TestSentinelEnabledProxies(t *testing.T) { } // add another proxy - tp2, err := NewTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tp2, err := newTestProxy(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -142,7 +142,7 @@ func TestSentinelEnabledProxies(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterDataEnabledProxiesNum(sm, 2, 3*initialClusterSpec.SleepInterval.Duration); err != nil { + if err := waitClusterDataEnabledProxiesNum(sm, 2, 3*initialClusterSpec.SleepInterval.Duration); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -152,7 +152,7 @@ func TestSentinelEnabledProxies(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterDataEnabledProxiesNum(sm, 1, 3*cluster.DefaultProxyTimeout); err != nil { + if err := waitClusterDataEnabledProxiesNum(sm, 1, 3*cluster.DefaultProxyTimeout); err != nil { t.Fatalf("unexpected err: %v", err) } @@ -162,7 +162,7 @@ func TestSentinelEnabledProxies(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - if err := WaitClusterDataEnabledProxiesNum(sm, 2, 6*initialClusterSpec.SleepInterval.Duration); err != nil { + if err := waitClusterDataEnabledProxiesNum(sm, 2, 6*initialClusterSpec.SleepInterval.Duration); err != nil { t.Fatalf("unexpected err: %v", err) } } diff --git a/tests/integration/standby_test.go b/tests/integration/standby_test.go index 907f3decf..147acc354 100644 --- a/tests/integration/standby_test.go +++ b/tests/integration/standby_test.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,22 +17,22 @@ package integration import ( "fmt" - "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/gofrs/uuid" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - "github.com/sorintlab/stolon/internal/store" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" ) func TestInitStandbyCluster(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -47,8 +48,8 @@ func TestInitStandbyCluster(t *testing.T) { pStorePath := filepath.Join(common.StorePrefix, primaryClusterName) psm := store.NewKVBackedStore(ptstore.store, pStorePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -58,7 +59,7 @@ func TestInitStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - pts, err := NewTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + pts, err := newTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -66,7 +67,7 @@ func TestInitStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer pts.Stop() - ptk, err := NewTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) + ptk, err := newTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -96,7 +97,7 @@ func TestInitStandbyCluster(t *testing.T) { storePath := filepath.Join(common.StorePrefix, clusterName) sm := store.NewKVBackedStore(tstore.store, storePath) - pgpass, err := ioutil.TempFile(dir, "pgpass") + pgpass, err := os.CreateTemp(dir, "pgpass") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -105,13 +106,13 @@ func TestInitStandbyCluster(t *testing.T) { } pgpass.Close() - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModePITR), - Role: cluster.ClusterRoleP(cluster.ClusterRoleStandby), + initialClusterSpec = &cluster.Spec{ + InitMode: &pitrCluster, + Role: &replica, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbyLag: cluster.Uint32P(50 * 1024), // limit lag to 50kiB + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB PGParameters: defaultPGParameters, PITRConfig: &cluster.PITRConfig{ DataRestoreCommand: fmt.Sprintf("PGPASSFILE=%s pg_basebackup -D %%d -h %s -p %s -U %s", pgpass.Name(), ptk.pgListenAddress, ptk.pgPort, ptk.pgReplUsername), @@ -127,7 +128,7 @@ func TestInitStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -135,7 +136,7 @@ func TestInitStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -164,7 +165,7 @@ func TestInitStandbyCluster(t *testing.T) { func TestPromoteStandbyCluster(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -180,8 +181,8 @@ func TestPromoteStandbyCluster(t *testing.T) { pStorePath := filepath.Join(common.StorePrefix, primaryClusterName) psm := store.NewKVBackedStore(ptstore.store, pStorePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -191,7 +192,7 @@ func TestPromoteStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - pts, err := NewTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + pts, err := newTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -199,7 +200,7 @@ func TestPromoteStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer pts.Stop() - ptk, err := NewTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) + ptk, err := newTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -229,7 +230,7 @@ func TestPromoteStandbyCluster(t *testing.T) { storePath := filepath.Join(common.StorePrefix, clusterName) sm := store.NewKVBackedStore(tstore.store, storePath) - pgpass, err := ioutil.TempFile(dir, "pgpass") + pgpass, err := os.CreateTemp(dir, "pgpass") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -238,13 +239,13 @@ func TestPromoteStandbyCluster(t *testing.T) { } pgpass.Close() - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModePITR), - Role: cluster.ClusterRoleP(cluster.ClusterRoleStandby), + initialClusterSpec = &cluster.Spec{ + InitMode: &pitrCluster, + Role: &replica, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbyLag: cluster.Uint32P(50 * 1024), // limit lag to 50kiB + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB PGParameters: defaultPGParameters, PITRConfig: &cluster.PITRConfig{ DataRestoreCommand: fmt.Sprintf("PGPASSFILE=%s pg_basebackup -D %%d -h %s -p %s -U %s", pgpass.Name(), ptk.pgListenAddress, ptk.pgPort, ptk.pgReplUsername), @@ -260,7 +261,7 @@ func TestPromoteStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -268,7 +269,7 @@ func TestPromoteStandbyCluster(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -294,13 +295,13 @@ func TestPromoteStandbyCluster(t *testing.T) { } // promote the standby cluster to a primary cluster - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "promote", "-y") + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "promote", "-y") if err != nil { t.Fatalf("unexpected err: %v", err) } // check that the cluster master has been promoted to a primary - if err := tk.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := tk.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } } @@ -308,13 +309,13 @@ func TestPromoteStandbyCluster(t *testing.T) { func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { t.Parallel() - dir, err := ioutil.TempDir("", "stolon") + dir, err := os.MkdirTemp("", "stolon") if err != nil { t.Fatalf("unexpected err: %v", err) } defer os.RemoveAll(dir) - archiveBackupDir, err := ioutil.TempDir(dir, "archivebackup") + archiveBackupDir, err := os.MkdirTemp(dir, "archivebackup") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -329,8 +330,8 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { pStorePath := filepath.Join(common.StorePrefix, primaryClusterName) psm := store.NewKVBackedStore(ptstore.store, pStorePath) - initialClusterSpec := &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew), + initialClusterSpec := &cluster.Spec{ + InitMode: &newCluster, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, @@ -344,7 +345,7 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - pts, err := NewTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + pts, err := newTestSentinel(t, dir, primaryClusterName, ptstore.storeBackend, primaryStoreEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -352,7 +353,7 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer pts.Stop() - ptk, err := NewTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) + ptk, err := newTestKeeper(t, dir, primaryClusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, ptstore.storeBackend, primaryStoreEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -382,7 +383,7 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { storePath := filepath.Join(common.StorePrefix, clusterName) sm := store.NewKVBackedStore(tstore.store, storePath) - pgpass, err := ioutil.TempFile(dir, "pgpass") + pgpass, err := os.CreateTemp(dir, "pgpass") if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -391,13 +392,13 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { } pgpass.Close() - initialClusterSpec = &cluster.ClusterSpec{ - InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModePITR), - Role: cluster.ClusterRoleP(cluster.ClusterRoleStandby), + initialClusterSpec = &cluster.Spec{ + InitMode: &pitrCluster, + Role: &replica, SleepInterval: &cluster.Duration{Duration: 2 * time.Second}, FailInterval: &cluster.Duration{Duration: 5 * time.Second}, ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second}, - MaxStandbyLag: cluster.Uint32P(50 * 1024), // limit lag to 50kiB + MaxStandbyLag: util.ToPtr(uint32(50 * 1024)), // limit lag to 50kiB PGParameters: defaultPGParameters, PITRConfig: &cluster.PITRConfig{ DataRestoreCommand: fmt.Sprintf("PGPASSFILE=%s pg_basebackup -Xs -D %%d -h %s -p %s -U %s", pgpass.Name(), ptk.pgListenAddress, ptk.pgPort, ptk.pgReplUsername), @@ -416,7 +417,7 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { t.Fatalf("unexpected err: %v", err) } - ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) + ts, err := newTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile)) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -424,7 +425,7 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { t.Fatalf("unexpected err: %v", err) } defer ts.Stop() - tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) + tk, err := newTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -456,13 +457,13 @@ func TestPromoteStandbyClusterArchiveRecovery(t *testing.T) { } // promote the standby cluster to a primary cluster - err = StolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "promote", "-y") + err = stolonCtl(t, clusterName, tstore.storeBackend, storeEndpoints, "promote", "-y") if err != nil { t.Fatalf("unexpected err: %v", err) } // check that the cluster master has been promoted to a primary - if err := tk.WaitDBRole(common.RoleMaster, nil, 30*time.Second); err != nil { + if err := tk.WaitDBRole(common.RolePrimary, nil, 30*time.Second); err != nil { t.Fatalf("unexpected err: %v", err) } } diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 7bddd8de3..fbb7960aa 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -1,3 +1,4 @@ +// Copyright 2026 PgVillage // Copyright 2015 Sorint.lab // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,8 +20,8 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" - "io/ioutil" "net" "os" "os/exec" @@ -33,31 +34,36 @@ import ( "testing" "time" - "github.com/sorintlab/stolon/internal/cluster" - "github.com/sorintlab/stolon/internal/common" - pg "github.com/sorintlab/stolon/internal/postgresql" - "github.com/sorintlab/stolon/internal/store" - "github.com/sorintlab/stolon/internal/util" + "github.com/Masterminds/semver/v3" + cluster "github.com/pgvillage-tools/stolon/api/v1" + "github.com/pgvillage-tools/stolon/internal/common" + pg "github.com/pgvillage-tools/stolon/internal/postgresql" + "github.com/pgvillage-tools/stolon/internal/store" + "github.com/pgvillage-tools/stolon/internal/util" "github.com/gofrs/uuid" - _ "github.com/lib/pq" + "github.com/sgotti/gexpect" ) const ( sleepInterval = 500 * time.Millisecond - MinPort = 2048 - MaxPort = 16384 + minPort = 2048 + maxPort = 16384 ) var ( defaultPGParameters = cluster.PGParameters{"log_destination": "stderr", "logging_collector": "false"} defaultStoreTimeout = 1 * time.Second + + errTimeout = errors.New("timeout") + + dbName = "postgres" ) -var curPort = MinPort +var curPort = minPort var portMutex = sync.Mutex{} func pgParametersWithDefaults(p cluster.PGParameters) cluster.PGParameters { @@ -71,16 +77,16 @@ func pgParametersWithDefaults(p cluster.PGParameters) cluster.PGParameters { return pd } -type Querier interface { - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (*sql.Rows, error) +type querier interface { + Exec(query string, args ...any) (sql.Result, error) + Query(query string, args ...any) (*sql.Rows, error) } -type ReplQuerier interface { - ReplQuery(query string, args ...interface{}) (*sql.Rows, error) +type replQuerier interface { + ReplQuery(query string, args ...any) (*sql.Rows, error) } -func GetPGParameters(q Querier) (common.Parameters, error) { +func getPGParameters(q querier) (common.Parameters, error) { var pgParameters = common.Parameters{} rows, err := q.Query("select name, setting, source from pg_settings") if err != nil { @@ -99,7 +105,7 @@ func GetPGParameters(q Querier) (common.Parameters, error) { return pgParameters, nil } -func GetSystemData(q ReplQuerier) (*pg.SystemData, error) { +func getSystemData(q replQuerier) (*pg.SystemData, error) { rows, err := q.ReplQuery("IDENTIFY_SYSTEM") if err != nil { return nil, err @@ -118,12 +124,12 @@ func GetSystemData(q ReplQuerier) (*pg.SystemData, error) { } return &sd, nil } - return nil, fmt.Errorf("query returned 0 rows") + return nil, errors.New("query returned 0 rows") } -func GetXLogPos(q ReplQuerier) (uint64, error) { +func getXLogPos(q replQuerier) (uint64, error) { // get the current master XLogPos - systemData, err := GetSystemData(q) + systemData, err := getSystemData(q) if err != nil { return 0, err } @@ -132,7 +138,7 @@ func GetXLogPos(q ReplQuerier) (uint64, error) { // getReplicatinSlots return existing replication slots (also temporary // replication slots on PostgreSQL > 10) -func getReplicationSlots(q Querier) ([]string, error) { +func getReplicationSlots(q querier) ([]string, error) { replSlots := []string{} rows, err := q.Query("select slot_name from pg_replication_slots") @@ -176,7 +182,7 @@ func waitReplicationSlots(q Querier, replSlots []string, timeout time.Duration) } */ -func waitStolonReplicationSlots(q Querier, replSlots []string, timeout time.Duration) error { +func waitStolonReplicationSlots(q querier, replSlots []string, timeout time.Duration) error { // prefix with stolon_ for i, slot := range replSlots { replSlots[i] = common.StolonName(slot) @@ -207,7 +213,7 @@ func waitStolonReplicationSlots(q Querier, replSlots []string, timeout time.Dura return fmt.Errorf("timeout waiting for replSlots %v, got: %v, last err: %v", replSlots, curReplSlots, err) } -func waitNotStolonReplicationSlots(q Querier, replSlots []string, timeout time.Duration) error { +func waitNotStolonReplicationSlots(q querier, replSlots []string, timeout time.Duration) error { sort.Strings(replSlots) start := time.Now() @@ -234,7 +240,7 @@ func waitNotStolonReplicationSlots(q Querier, replSlots []string, timeout time.D return fmt.Errorf("timeout waiting for replSlots %v, got: %v, last err: %v", replSlots, curReplSlots, err) } -type Process struct { +type process struct { t *testing.T uid string name string @@ -243,7 +249,7 @@ type Process struct { bin string } -func (p *Process) start() error { +func (p *process) start() error { if p.cmd != nil { panic(fmt.Errorf("%s: cmd not cleanly stopped", p.uid)) } @@ -266,7 +272,7 @@ func (p *Process) start() error { return nil } -func (p *Process) Start() error { +func (p *process) Start() error { if err := p.start(); err != nil { return err } @@ -274,11 +280,11 @@ func (p *Process) Start() error { return nil } -func (p *Process) StartExpect() error { +func (p *process) StartExpect() error { return p.start() } -func (p *Process) Signal(sig os.Signal) error { +func (p *process) Signal(sig os.Signal) error { p.t.Logf("signalling %s %s with %s", p.name, p.uid, sig) if p.cmd == nil { panic(fmt.Errorf("p: %s, cmd is empty", p.uid)) @@ -286,7 +292,7 @@ func (p *Process) Signal(sig os.Signal) error { return p.cmd.Cmd.Process.Signal(sig) } -func (p *Process) Kill() { +func (p *process) Kill() { p.t.Logf("killing %s %s", p.name, p.uid) if p.cmd == nil { panic(fmt.Errorf("p: %s, cmd is empty", p.uid)) @@ -296,7 +302,7 @@ func (p *Process) Kill() { p.cmd = nil } -func (p *Process) Stop() { +func (p *process) Stop() { p.t.Logf("stopping %s %s", p.name, p.uid) if p.cmd == nil { panic(fmt.Errorf("p: %s, cmd is empty", p.uid)) @@ -307,7 +313,7 @@ func (p *Process) Stop() { p.cmd = nil } -func (p *Process) Wait(timeout time.Duration) error { +func (p *process) Wait(timeout time.Duration) error { timeoutCh := time.NewTimer(timeout).C endCh := make(chan error) go func() { @@ -316,15 +322,15 @@ func (p *Process) Wait(timeout time.Duration) error { }() select { case <-timeoutCh: - return fmt.Errorf("timeout waiting on process") + return errors.New("timeout waiting on process") case <-endCh: return nil } } -type TestKeeper struct { +type testKeeper struct { t *testing.T - Process + process dataDir string pgListenAddress string pgPort string @@ -336,7 +342,13 @@ type TestKeeper struct { rdb *sql.DB } -func NewTestKeeperWithID(t *testing.T, dir, uid, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, storeBackend store.Backend, storeEndpoints string, a ...string) (*TestKeeper, error) { +func newTestKeeperWithID( + t *testing.T, + dir, uid, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, + storeBackend store.BackendType, + storeEndpoints string, + a ...string, +) (*testKeeper, error) { args := []string{} dataDir := filepath.Join(dir, fmt.Sprintf("st%s", uid)) @@ -369,7 +381,7 @@ func NewTestKeeperWithID(t *testing.T, dir, uid, clusterName, pgSUUsername, pgSU "password": pgSUPassword, "host": pgListenAddress, "port": pgPort, - "dbname": "postgres", + "dbname": dbName, "sslmode": "disable", } @@ -378,30 +390,30 @@ func NewTestKeeperWithID(t *testing.T, dir, uid, clusterName, pgSUUsername, pgSU "password": pgReplPassword, "host": pgListenAddress, "port": pgPort, - "dbname": "postgres", + "dbname": dbName, "sslmode": "disable", "replication": "1", } connString := connParams.ConnString() - db, err := sql.Open("postgres", connString) + db, err := sql.Open(dbName, connString) if err != nil { return nil, err } replConnString := replConnParams.ConnString() - rdb, err := sql.Open("postgres", replConnString) + rdb, err := sql.Open(dbName, replConnString) if err != nil { return nil, err } bin := os.Getenv("STKEEPER_BIN") if bin == "" { - return nil, fmt.Errorf("missing STKEEPER_BIN env") + return nil, errors.New("missing STKEEPER_BIN env") } - tk := &TestKeeper{ + tk := &testKeeper{ t: t, - Process: Process{ + process: process{ t: t, uid: uid, name: "keeper", @@ -421,17 +433,24 @@ func NewTestKeeperWithID(t *testing.T, dir, uid, clusterName, pgSUUsername, pgSU return tk, nil } -func NewTestKeeper(t *testing.T, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, storeBackend store.Backend, storeEndpoints string, a ...string) (*TestKeeper, error) { +func newTestKeeper( + t *testing.T, + dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, + storeBackend store.BackendType, + storeEndpoints string, + a ...string, +) (*testKeeper, error) { u := uuid.Must(uuid.NewV4()) uid := fmt.Sprintf("%x", u[:4]) - return NewTestKeeperWithID(t, dir, uid, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, storeBackend, storeEndpoints, a...) + return newTestKeeperWithID(t, dir, uid, clusterName, pgSUUsername, pgSUPassword, + pgReplUsername, pgReplPassword, storeBackend, storeEndpoints, a...) } -func (tk *TestKeeper) PGDataVersion() (int, int, error) { +func (tk *testKeeper) PGDataVersion() (*semver.Version, error) { fh, err := os.Open(filepath.Join(tk.dataDir, "postgres", "PG_VERSION")) if err != nil { - return 0, 0, fmt.Errorf("failed to read PG_VERSION: %v", err) + return nil, fmt.Errorf("failed to read PG_VERSION: %v", err) } defer fh.Close() @@ -441,17 +460,17 @@ func (tk *TestKeeper) PGDataVersion() (int, int, error) { scanner.Scan() version := scanner.Text() - return pg.ParseVersion(version) + return semver.NewVersion(version) } -func (tk *TestKeeper) GetPrimaryConninfo() (pg.ConnParams, error) { - maj, _, err := tk.PGDataVersion() +func (tk *testKeeper) GetPrimaryConninfo() (pg.ConnParams, error) { + version, err := tk.PGDataVersion() if err != nil { return nil, err } confFile := "recovery.conf" - if maj >= 12 { + if version.GreaterThanEqual(pg.V12) { confFile = "postgresql.conf" } regex := regexp.MustCompile(`\s*primary_conninfo\s*=\s*'(.*)'$`) @@ -474,7 +493,7 @@ func (tk *TestKeeper) GetPrimaryConninfo() (pg.ConnParams, error) { return nil, nil } -func (tk *TestKeeper) Exec(query string, args ...interface{}) (sql.Result, error) { +func (tk *testKeeper) Exec(query string, args ...any) (sql.Result, error) { res, err := tk.db.Exec(query, args...) if err != nil { return nil, err @@ -483,7 +502,7 @@ func (tk *TestKeeper) Exec(query string, args ...interface{}) (sql.Result, error return res, nil } -func (tk *TestKeeper) Query(query string, args ...interface{}) (*sql.Rows, error) { +func (tk *testKeeper) Query(query string, args ...any) (*sql.Rows, error) { res, err := tk.db.Query(query, args...) if err != nil { return nil, err @@ -492,7 +511,7 @@ func (tk *TestKeeper) Query(query string, args ...interface{}) (*sql.Rows, error return res, nil } -func (tk *TestKeeper) ReplQuery(query string, args ...interface{}) (*sql.Rows, error) { +func (tk *testKeeper) ReplQuery(query string, args ...any) (*sql.Rows, error) { res, err := tk.rdb.Query(query, args...) if err != nil { return nil, err @@ -501,13 +520,13 @@ func (tk *TestKeeper) ReplQuery(query string, args ...interface{}) (*sql.Rows, e return res, nil } -func (tk *TestKeeper) SwitchWals(times int) error { - maj, _, err := tk.PGDataVersion() +func (tk *testKeeper) SwitchWals(times int) error { + version, err := tk.PGDataVersion() if err != nil { return err } var switchLogFunc string - if maj < 10 { + if version.LessThan(pg.V10) { switchLogFunc = "select pg_switch_xlog()" } else { switchLogFunc = "select pg_switch_wal()" @@ -530,12 +549,12 @@ func (tk *TestKeeper) SwitchWals(times int) error { return nil } -func (tk *TestKeeper) CheckPoint() error { +func (tk *testKeeper) CheckPoint() error { _, err := tk.Exec("CHECKPOINT") return err } -func (tk *TestKeeper) WaitDBUp(timeout time.Duration) error { +func (tk *testKeeper) WaitDBUp(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { _, err := tk.Exec("select 1") @@ -545,10 +564,10 @@ func (tk *TestKeeper) WaitDBUp(timeout time.Duration) error { tk.t.Logf("tk: %v, error: %v", tk.uid, err) time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func (tk *TestKeeper) WaitDBDown(timeout time.Duration) error { +func (tk *testKeeper) WaitDBDown(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { _, err := tk.Exec("select 1") @@ -557,10 +576,10 @@ func (tk *TestKeeper) WaitDBDown(timeout time.Duration) error { } time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func (tk *TestKeeper) GetPGProcess() (*os.Process, error) { +func (tk *testKeeper) GetPGProcess() (*os.Process, error) { fh, err := os.Open(filepath.Join(tk.dataDir, "postgres/postmaster.pid")) if err != nil { return nil, err @@ -570,7 +589,7 @@ func (tk *TestKeeper) GetPGProcess() (*os.Process, error) { scanner := bufio.NewScanner(fh) scanner.Split(bufio.ScanLines) if !scanner.Scan() { - return nil, fmt.Errorf("not enough lines in pid file") + return nil, errors.New("not enough lines in pid file") } pidStr := scanner.Text() pid, err := strconv.Atoi(string(pidStr)) @@ -580,7 +599,7 @@ func (tk *TestKeeper) GetPGProcess() (*os.Process, error) { return os.FindProcess(pid) } -func (tk *TestKeeper) SignalPG(sig os.Signal) error { +func (tk *testKeeper) SignalPG(sig os.Signal) error { p, err := tk.GetPGProcess() if err != nil { return err @@ -588,7 +607,7 @@ func (tk *TestKeeper) SignalPG(sig os.Signal) error { return p.Signal(sig) } -func (tk *TestKeeper) isInRecovery() (bool, error) { +func (tk *testKeeper) isInRecovery() (bool, error) { rows, err := tk.Query("SELECT pg_is_in_recovery from pg_is_in_recovery()") if err != nil { return false, err @@ -604,10 +623,10 @@ func (tk *TestKeeper) isInRecovery() (bool, error) { } return false, nil } - return false, fmt.Errorf("no rows returned") + return false, errors.New("no rows returned") } -func (tk *TestKeeper) WaitDBRole(r common.Role, ptk *TestKeeper, timeout time.Duration) error { +func (tk *testKeeper) WaitDBRole(r common.Role, ptk *testKeeper, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { time.Sleep(sleepInterval) @@ -620,10 +639,10 @@ func (tk *TestKeeper) WaitDBRole(r common.Role, ptk *TestKeeper, timeout time.Du if err != nil { continue } - if !ok && r == common.RoleMaster { + if !ok && r == common.RolePrimary { return nil } - if ok && r == common.RoleStandby { + if ok && r == common.RoleReplica { return nil } } else { @@ -641,25 +660,25 @@ func (tk *TestKeeper) WaitDBRole(r common.Role, ptk *TestKeeper, timeout time.Du continue } if conninfo["host"] == ptk.pgListenAddress && conninfo["port"] == ptk.pgPort { - if r == common.RoleMaster { + if r == common.RolePrimary { return nil } } else { - if r == common.RoleStandby { + if r == common.RoleReplica { return nil } } } } - return fmt.Errorf("timeout") + return errTimeout } -func (tk *TestKeeper) WaitPGParameter(parameter, value string, timeout time.Duration) error { +func (tk *testKeeper) WaitPGParameter(parameter, value string, timeout time.Duration) error { latestValue := "" start := time.Now() for time.Now().Add(-timeout).Before(start) { - pgParameters, err := GetPGParameters(tk) + pgParameters, err := getPGParameters(tk) if err != nil { goto end } @@ -673,8 +692,8 @@ func (tk *TestKeeper) WaitPGParameter(parameter, value string, timeout time.Dura return fmt.Errorf("timeout waiting for pgParamater %q (%q) to equal %q", parameter, latestValue, value) } -func (tk *TestKeeper) GetPGParameters() (common.Parameters, error) { - return GetPGParameters(tk) +func (tk *testKeeper) GetPGParameters() (common.Parameters, error) { + return getPGParameters(tk) } /* @@ -703,12 +722,19 @@ func waitChecks(timeout time.Duration, fns ...CheckFunc) error { } */ -type TestSentinel struct { +type testSentinel struct { t *testing.T - Process + process } -func NewTestSentinel(t *testing.T, dir string, clusterName string, storeBackend store.Backend, storeEndpoints string, a ...string) (*TestSentinel, error) { +func newTestSentinel( + t *testing.T, + _ string, + clusterName string, + storeBackend store.BackendType, + storeEndpoints string, + a ...string, +) (*testSentinel, error) { u := uuid.Must(uuid.NewV4()) uid := fmt.Sprintf("%x", u[:4]) @@ -723,11 +749,11 @@ func NewTestSentinel(t *testing.T, dir string, clusterName string, storeBackend bin := os.Getenv("STSENTINEL_BIN") if bin == "" { - return nil, fmt.Errorf("missing STSENTINEL_BIN env") + return nil, errors.New("missing STSENTINEL_BIN env") } - ts := &TestSentinel{ + ts := &testSentinel{ t: t, - Process: Process{ + process: process{ t: t, uid: uid, name: "sentinel", @@ -738,16 +764,23 @@ func NewTestSentinel(t *testing.T, dir string, clusterName string, storeBackend return ts, nil } -type TestProxy struct { +type testProxy struct { t *testing.T - Process + process listenAddress string port string db *sql.DB rdb *sql.DB } -func NewTestProxy(t *testing.T, dir string, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, storeBackend store.Backend, storeEndpoints string, a ...string) (*TestProxy, error) { +func newTestProxy( + t *testing.T, + _ string, + clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword string, + storeBackend store.BackendType, + storeEndpoints string, + a ...string, +) (*testProxy, error) { u := uuid.Must(uuid.NewV4()) uid := fmt.Sprintf("%x", u[:4]) @@ -772,7 +805,7 @@ func NewTestProxy(t *testing.T, dir string, clusterName, pgSUUsername, pgSUPassw "password": pgSUPassword, "host": listenAddress, "port": port, - "dbname": "postgres", + "dbname": dbName, "sslmode": "disable", } @@ -781,30 +814,30 @@ func NewTestProxy(t *testing.T, dir string, clusterName, pgSUUsername, pgSUPassw "password": pgReplPassword, "host": listenAddress, "port": port, - "dbname": "postgres", + "dbname": dbName, "sslmode": "disable", "replication": "1", } connString := connParams.ConnString() - db, err := sql.Open("postgres", connString) + db, err := sql.Open(dbName, connString) if err != nil { return nil, err } replConnString := replConnParams.ConnString() - rdb, err := sql.Open("postgres", replConnString) + rdb, err := sql.Open(dbName, replConnString) if err != nil { return nil, err } bin := os.Getenv("STPROXY_BIN") if bin == "" { - return nil, fmt.Errorf("missing STPROXY_BIN env") + return nil, errors.New("missing STPROXY_BIN env") } - tp := &TestProxy{ + tp := &testProxy{ t: t, - Process: Process{ + process: process{ t: t, uid: uid, name: "proxy", @@ -819,7 +852,7 @@ func NewTestProxy(t *testing.T, dir string, clusterName, pgSUUsername, pgSUPassw return tp, nil } -func (tp *TestProxy) WaitListening(timeout time.Duration) error { +func (tp *testProxy) WaitListening(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { _, err := net.DialTimeout("tcp", net.JoinHostPort(tp.listenAddress, tp.port), timeout-time.Since(start)) @@ -828,15 +861,15 @@ func (tp *TestProxy) WaitListening(timeout time.Duration) error { } time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func (tp *TestProxy) CheckListening() bool { +func (tp *testProxy) CheckListening() bool { _, err := net.Dial("tcp", net.JoinHostPort(tp.listenAddress, tp.port)) return err == nil } -func (tp *TestProxy) WaitNotListening(timeout time.Duration) error { +func (tp *testProxy) WaitNotListening(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { _, err := net.DialTimeout("tcp", net.JoinHostPort(tp.listenAddress, tp.port), timeout-time.Since(start)) @@ -845,10 +878,10 @@ func (tp *TestProxy) WaitNotListening(timeout time.Duration) error { } time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func (tp *TestProxy) Exec(query string, args ...interface{}) (sql.Result, error) { +func (tp *testProxy) Exec(query string, args ...any) (sql.Result, error) { res, err := tp.db.Exec(query, args...) if err != nil { return nil, err @@ -857,7 +890,7 @@ func (tp *TestProxy) Exec(query string, args ...interface{}) (sql.Result, error) return res, nil } -func (tp *TestProxy) Query(query string, args ...interface{}) (*sql.Rows, error) { +func (tp *testProxy) Query(query string, args ...any) (*sql.Rows, error) { res, err := tp.db.Query(query, args...) if err != nil { return nil, err @@ -866,7 +899,7 @@ func (tp *TestProxy) Query(query string, args ...interface{}) (*sql.Rows, error) return res, nil } -func (tp *TestProxy) ReplQuery(query string, args ...interface{}) (*sql.Rows, error) { +func (tp *testProxy) ReplQuery(query string, args ...any) (*sql.Rows, error) { res, err := tp.rdb.Query(query, args...) if err != nil { return nil, err @@ -875,15 +908,22 @@ func (tp *TestProxy) ReplQuery(query string, args ...interface{}) (*sql.Rows, er return res, nil } -func (tp *TestProxy) GetPGParameters() (common.Parameters, error) { - return GetPGParameters(tp) +func (tp *testProxy) GetPGParameters() (common.Parameters, error) { + return getPGParameters(tp) } -func (tp *TestProxy) WaitRightMaster(tk *TestKeeper, timeout time.Duration) error { - return tk.WaitPGParameter("port", tk.pgPort, timeout) +func (tp *testProxy) WaitRightMaster(tk *testKeeper, timeout time.Duration) error { + const waitParam = "port" + return tk.WaitPGParameter(waitParam, tk.pgPort, timeout) } -func StolonCtl(t *testing.T, clusterName string, storeBackend store.Backend, storeEndpoints string, a ...string) error { +func stolonCtl( + t *testing.T, + clusterName string, + storeBackend store.BackendType, + storeEndpoints string, + a ...string, +) error { args := []string{} args = append(args, fmt.Sprintf("--cluster-name=%s", clusterName)) args = append(args, fmt.Sprintf("--store-backend=%s", storeBackend)) @@ -894,7 +934,7 @@ func StolonCtl(t *testing.T, clusterName string, storeBackend store.Backend, sto bin := os.Getenv("STCTL_BIN") if bin == "" { - return fmt.Errorf("missing STCTL_BIN env") + return errors.New("missing STCTL_BIN env") } cmd := exec.Command(bin, args...) pr, pw, err := os.Pipe() @@ -913,30 +953,32 @@ func StolonCtl(t *testing.T, clusterName string, storeBackend store.Backend, sto return cmd.Run() } -type TestStore struct { +type testStore struct { t *testing.T - Process + process listenAddress string port string store store.KVStore - storeBackend store.Backend + storeBackend store.BackendType } -func NewTestStore(t *testing.T, dir string, a ...string) (*TestStore, error) { - storeBackend := store.Backend(os.Getenv("STOLON_TEST_STORE_BACKEND")) +func newTestStore(t *testing.T, dir string, a ...string) (*testStore, error) { + storeBackend := store.BackendType(os.Getenv("STOLON_TEST_STORE_BACKEND")) switch storeBackend { case "consul": - return NewTestConsul(t, dir, a...) + return newTestConsul(t, dir, a...) case "etcd": storeBackend = "etcdv2" fallthrough case "etcdv2", "etcdv3": - return NewTestEtcd(t, dir, storeBackend, a...) + return newTestEtcd(t, dir, storeBackend, a...) } - return nil, fmt.Errorf("wrong store backend") + return nil, errors.New("wrong store backend") } -func NewTestEtcd(t *testing.T, dir string, backend store.Backend, a ...string) (*TestStore, error) { +func newTestEtcd(t *testing.T, dir string, backend store.BackendType, a ...string) (*testStore, error) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() u := uuid.Must(uuid.NewV4()) uid := fmt.Sprintf("%x", u[:4]) @@ -964,22 +1006,22 @@ func NewTestEtcd(t *testing.T, dir string, backend store.Backend, a ...string) ( storeEndpoints := fmt.Sprintf("%s:%s", listenAddress, port) storeConfig := store.Config{ - Backend: store.Backend(backend), + Backend: store.BackendType(backend), Endpoints: storeEndpoints, Timeout: defaultStoreTimeout, } - kvstore, err := store.NewKVStore(storeConfig) + kvstore, err := store.NewKVStore(ctx, storeConfig) if err != nil { return nil, fmt.Errorf("cannot create store: %v", err) } bin := os.Getenv("ETCD_BIN") if bin == "" { - return nil, fmt.Errorf("missing ETCD_BIN env") + return nil, errors.New("missing ETCD_BIN env") } - tstore := &TestStore{ + tstore := &testStore{ t: t, - Process: Process{ + process: process{ t: t, uid: uid, name: "etcd", @@ -994,7 +1036,9 @@ func NewTestEtcd(t *testing.T, dir string, backend store.Backend, a ...string) ( return tstore, nil } -func NewTestConsul(t *testing.T, dir string, a ...string) (*TestStore, error) { +func newTestConsul(t *testing.T, dir string, a ...string) (*testStore, error) { + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() u := uuid.Must(uuid.NewV4()) uid := fmt.Sprintf("%x", u[:4]) @@ -1052,18 +1096,18 @@ func NewTestConsul(t *testing.T, dir string, a ...string) (*TestStore, error) { Endpoints: storeEndpoints, Timeout: defaultStoreTimeout, } - kvstore, err := store.NewKVStore(storeConfig) + kvstore, err := store.NewKVStore(ctx, storeConfig) if err != nil { return nil, fmt.Errorf("cannot create store: %v", err) } bin := os.Getenv("CONSUL_BIN") if bin == "" { - return nil, fmt.Errorf("missing CONSUL_BIN env") + return nil, errors.New("missing CONSUL_BIN env") } - ts := &TestStore{ + ts := &testStore{ t: t, - Process: Process{ + process: process{ t: t, uid: uid, name: "consul", @@ -1078,7 +1122,7 @@ func NewTestConsul(t *testing.T, dir string, a ...string) (*TestStore, error) { return ts, nil } -func (ts *TestStore) WaitUp(timeout time.Duration) error { +func (ts *testStore) WaitUp(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { ctx, cancel := context.WithTimeout(context.Background(), defaultStoreTimeout) @@ -1094,10 +1138,10 @@ func (ts *TestStore) WaitUp(timeout time.Duration) error { time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func (ts *TestStore) WaitDown(timeout time.Duration) error { +func (ts *testStore) WaitDown(timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { ctx, cancel := context.WithTimeout(context.Background(), defaultStoreTimeout) @@ -1109,10 +1153,10 @@ func (ts *TestStore) WaitDown(timeout time.Duration) error { time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterDataUpdated(e *store.KVBackedStore, timeout time.Duration) error { +func waitClusterDataUpdated(e *store.KVBackedStore, timeout time.Duration) error { icd, _, err := e.GetClusterData(context.TODO()) if err != nil { return fmt.Errorf("unexpected err: %v", err) @@ -1129,33 +1173,33 @@ func WaitClusterDataUpdated(e *store.KVBackedStore, timeout time.Duration) error end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterDataWithMaster(e *store.KVBackedStore, timeout time.Duration) (string, error) { +func waitClusterDataWithMaster(e *store.KVBackedStore, timeout time.Duration) (string, error) { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) if err != nil || cd == nil { goto end } - if cd.Cluster.Status.Phase == cluster.ClusterPhaseNormal && cd.Cluster.Status.Master != "" { + if cd.Cluster.Status.Phase == cluster.Normal && cd.Cluster.Status.Master != "" { return cd.DBs[cd.Cluster.Status.Master].Spec.KeeperUID, nil } end: time.Sleep(sleepInterval) } - return "", fmt.Errorf("timeout") + return "", errTimeout } -func WaitClusterDataMaster(master string, e *store.KVBackedStore, timeout time.Duration) error { +func waitClusterDataMaster(master string, e *store.KVBackedStore, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) if err != nil || cd == nil { goto end } - if cd.Cluster.Status.Phase == cluster.ClusterPhaseNormal && cd.Cluster.Status.Master != "" { + if cd.Cluster.Status.Phase == cluster.Normal && cd.Cluster.Status.Master != "" { if cd.DBs[cd.Cluster.Status.Master].Spec.KeeperUID == master { return nil } @@ -1163,10 +1207,10 @@ func WaitClusterDataMaster(master string, e *store.KVBackedStore, timeout time.D end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterDataKeeperInitialized(keeperUID string, e *store.KVBackedStore, timeout time.Duration) error { +func waitClusterDataKeeperInitialized(keeperUID string, e *store.KVBackedStore, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) @@ -1184,13 +1228,14 @@ func WaitClusterDataKeeperInitialized(keeperUID string, e *store.KVBackedStore, end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } // WaitClusterDataSynchronousStandbys waits for: // * synchronous standby defined in masterdb spec // * synchronous standby reported from masterdb status -func WaitClusterDataSynchronousStandbys(synchronousStandbys []string, e *store.KVBackedStore, timeout time.Duration) error { +func WaitClusterDataSynchronousStandbys(synchronousStandbys []string, e *store.KVBackedStore, + timeout time.Duration) error { sort.Strings(synchronousStandbys) start := time.Now() for time.Now().Add(-timeout).Before(start) { @@ -1198,7 +1243,7 @@ func WaitClusterDataSynchronousStandbys(synchronousStandbys []string, e *store.K if err != nil || cd == nil { goto end } - if cd.Cluster.Status.Phase == cluster.ClusterPhaseNormal && cd.Cluster.Status.Master != "" { + if cd.Cluster.Status.Phase == cluster.Normal && cd.Cluster.Status.Master != "" { masterDB := cd.DBs[cd.Cluster.Status.Master] // get keepers for db spec synchronousStandbys keepersUIDs := []string{} @@ -1230,10 +1275,10 @@ func WaitClusterDataSynchronousStandbys(synchronousStandbys []string, e *store.K end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterPhase(e *store.KVBackedStore, phase cluster.ClusterPhase, timeout time.Duration) error { +func waitClusterPhase(e *store.KVBackedStore, phase cluster.Phase, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) @@ -1246,10 +1291,10 @@ func WaitClusterPhase(e *store.KVBackedStore, phase cluster.ClusterPhase, timeou end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitNumDBs(e *store.KVBackedStore, n int, timeout time.Duration) error { +func waitNumDBs(e *store.KVBackedStore, n int, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) @@ -1262,10 +1307,10 @@ func WaitNumDBs(e *store.KVBackedStore, n int, timeout time.Duration) error { end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitStandbyKeeper(e *store.KVBackedStore, keeperUID string, timeout time.Duration) error { +func waitStandbyKeeper(e *store.KVBackedStore, keeperUID string, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) @@ -1277,17 +1322,17 @@ func WaitStandbyKeeper(e *store.KVBackedStore, keeperUID string, timeout time.Du if db.UID == cd.Cluster.Status.Master { continue } - if db.Spec.KeeperUID == keeperUID && db.Spec.Role == common.RoleStandby { + if db.Spec.KeeperUID == keeperUID && db.Spec.Role == common.RoleReplica { return nil } } end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterDataKeepers(keepersUIDs []string, e *store.KVBackedStore, timeout time.Duration) error { +func waitClusterDataKeepers(keepersUIDs []string, e *store.KVBackedStore, timeout time.Duration) error { start := time.Now() for time.Now().Add(-timeout).Before(start) { cd, _, err := e.GetClusterData(context.TODO()) @@ -1307,12 +1352,13 @@ func WaitClusterDataKeepers(keepersUIDs []string, e *store.KVBackedStore, timeou end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } // WaitClusterSyncedXLogPos waits for all the specified keepers to have the same // reported XLogPos and that it's >= than master XLogPos -func WaitClusterSyncedXLogPos(keepers []*TestKeeper, xLogPos uint64, e *store.KVBackedStore, timeout time.Duration) error { +func WaitClusterSyncedXLogPos(keepers []*testKeeper, xLogPos uint64, e *store.KVBackedStore, + timeout time.Duration) error { keepersUIDs := []string{} for _, sk := range keepers { keepersUIDs = append(keepersUIDs, sk.uid) @@ -1355,10 +1401,10 @@ func WaitClusterSyncedXLogPos(keepers []*TestKeeper, xLogPos uint64, e *store.KV end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } -func WaitClusterDataEnabledProxiesNum(e *store.KVBackedStore, n int, timeout time.Duration) error { +func waitClusterDataEnabledProxiesNum(e *store.KVBackedStore, n int, timeout time.Duration) error { // TODO(sgotti) find a way to retrieve the proxies internally generated uids // and check for them instead of relying only on the number of proxies start := time.Now() @@ -1373,7 +1419,7 @@ func WaitClusterDataEnabledProxiesNum(e *store.KVBackedStore, n int, timeout tim end: time.Sleep(sleepInterval) } - return fmt.Errorf("timeout") + return errTimeout } func testFreeTCPPort(port int) error { @@ -1400,7 +1446,7 @@ func getFreePort(tcp bool, udp bool) (string, string, error) { defer portMutex.Unlock() if !tcp && !udp { - return "", "", fmt.Errorf("at least one of tcp or udp port shuld be required") + return "", "", errors.New("at least one of tcp or udp port shuld be required") } localhostIP, err := net.ResolveIPAddr("ip", "localhost") if err != nil { @@ -1408,8 +1454,8 @@ func getFreePort(tcp bool, udp bool) (string, string, error) { } for { curPort++ - if curPort > MaxPort { - return "", "", fmt.Errorf("all available ports to test have been exausted") + if curPort > maxPort { + return "", "", errors.New("all available ports to test have been exausted") } if tcp { if err := testFreeTCPPort(curPort); err != nil { @@ -1425,12 +1471,12 @@ func getFreePort(tcp bool, udp bool) (string, string, error) { } } -func writeClusterSpec(dir string, cs *cluster.ClusterSpec) (string, error) { +func writeClusterSpec(dir string, cs *cluster.Spec) (string, error) { csj, err := json.Marshal(cs) if err != nil { return "", err } - tmpFile, err := ioutil.TempFile(dir, "initial-cluster-spec.json") + tmpFile, err := os.CreateTemp(dir, "initial-cluster-spec.json") if err != nil { return "", err } @@ -1439,5 +1485,4 @@ func writeClusterSpec(dir string, cs *cluster.ClusterSpec) (string, error) { return "", err } return tmpFile.Name(), nil - } diff --git a/tests/testcerts/ca.crt b/tests/testcerts/ca.crt new file mode 100644 index 000000000..7d6fc04db --- /dev/null +++ b/tests/testcerts/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUNH+fwZvgPoTS55NSQs726ymCP4IwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yNjAxMjYyMzA4MDNaFw0yNjAyMjUy +MzA4MDNaMBIxEDAOBgNVBAMMB3Rlc3QtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCueJVzNEOHDQ/xnaESc8Rb5Kb76gVC1jhrwkfLNV63h75RpHYk +gLhk2/+rU8nxMgdksEPp4MzCLjKt21BRVEBKHz69qrtwjH6+/YOPV0gfhLaY05nV +uKGRa720Uxrw2XI7nrdmy9WCsUjMtzwn5zKHjrdJ+sVixPH6ut27XNGPqgBvXdHk +Zw2V8EAA6kLy286WamRQkLXcaB85Q1wweoXXnJ/Fe5VkxpxpTLxZO9v6fOYfGzVe +SWECwqCEqz3LAe0C5wCmXs7Pfq077rL2fvLr34/16D3H6wTC0qJZECmsqnZ+919C +uq4gWnijSCPPT/bD7Z5LBzAvhoals65OP/k9AgMBAAGjUzBRMB0GA1UdDgQWBBSk +vksk1fypmuMqQ9/ybEfceBdY7zAfBgNVHSMEGDAWgBSkvksk1fypmuMqQ9/ybEfc +eBdY7zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB0DFAbTzy7 +8W9W5U2gXDeJ7fkm2QkQXbhQt4HXe/C3h3mWiLynqSf8UwSs7XM2Tai6UDcPrjRf +4z95XYRb3dHYQ4eWvMjuDuSimFeNysPyPBbE6/mH1B3ydJcvF4bNT5eGVHupINWF +fZ5i4R+R9dxdgL32r+g37fm7Q2ANE3guVJ07iUYR1dv5y6wCbxQRtnDyfT9pqrNj +YFzMyY0kSztM1PwRv1zJdEmFN5wY3x9UlAK9aaoB/dgTmRm1P7e/Z4loRuS71m27 +OyHA5ff+ov2dk5Vy41Zvvl5NY1IVXS6WwNdwveMvO+TTmXZ2VFmls5Id8T61ix3t +sYRDccepvZTK +-----END CERTIFICATE----- diff --git a/tests/testcerts/server.crt b/tests/testcerts/server.crt new file mode 100644 index 000000000..8e9b81ff9 --- /dev/null +++ b/tests/testcerts/server.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9jCCAd6gAwIBAgIULfK05+gb7j88uriUDYPI11oDTYwwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC1jYTAeFw0yNjAxMjYyMzA4MDNaFw0yNzAxMjYy +MzA4MDNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAK07XW6fd9KAL+rWQ/mF0QX2yhOSZdj79F93we4nsKxjonO+ +6ZTY0ei1WNBYbc8RyH/GBKffvI859VLlcLcUSrxjVZGooMyCdu0W4k80FQt1FKPt +QJHJZR0+MjyUDoZmrsFgUoUpK0dBwpn1QAAE89VNDUnx+CEr9rKVvh9P6JSm0Ep9 +vt+yIPdMtkH9X+CPEXW/xvI+66nQJ+bMrQBQBkqLAppp96q0TtcFJr9TYtLN4hOT +O7ViQQF7M5HAvlNlTAgBilSy7YOu9yWKVTH3S7P5O3E42h71V0BPYmtbSrxw3DYM +P9kL2P30L4itImQrX0tEcAXIXAKVV4vBpkFBnHkCAwEAAaNCMEAwHQYDVR0OBBYE +FM+NWCf2hrgMW121xr+xaS5leQSvMB8GA1UdIwQYMBaAFKS+SyTV/Kma4ypD3/Js +R9x4F1jvMA0GCSqGSIb3DQEBCwUAA4IBAQCKmKcJHlo5RxNay3YnQBYfud2GWobn +K3CUsXRW8V/qkyG7Duur7Vcw8EdEgXfCEX+xvdVl2iJw1XC08+eZoqvJmuwRjg+j +3kH++Cgq7PToQLq50Tr3CfSfRXzNiYWkqB6UVUvpOa7xQbS/JZqbYQgpUPiCo+5x +nrvz0GpYrrGajZUYumdtGwAd42Q6FersIWIyGxx7YmfKW1IqXSwrm4eK2lXE0lPQ +XFxPAVZO8EsxeEtaQMJ9aHuN8ODKz63l5Gzh4jpE9aJLAOmSKNFbEKG5LoQA9JQz +teeXqPRWKAg8CM0JXDEfak3XT6uHnMt+gnfH6xNQnY7d4QXjR/S5Ewe4 +-----END CERTIFICATE----- diff --git a/tests/testcerts/server.key b/tests/testcerts/server.key new file mode 100644 index 000000000..4a254bfdf --- /dev/null +++ b/tests/testcerts/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtO11un3fSgC/q +1kP5hdEF9soTkmXY+/Rfd8HuJ7CsY6JzvumU2NHotVjQWG3PEch/xgSn37yPOfVS +5XC3FEq8Y1WRqKDMgnbtFuJPNBULdRSj7UCRyWUdPjI8lA6GZq7BYFKFKStHQcKZ +9UAABPPVTQ1J8fghK/aylb4fT+iUptBKfb7fsiD3TLZB/V/gjxF1v8byPuup0Cfm +zK0AUAZKiwKaafeqtE7XBSa/U2LSzeITkzu1YkEBezORwL5TZUwIAYpUsu2Drvcl +ilUx90uz+TtxONoe9VdAT2JrW0q8cNw2DD/ZC9j99C+IrSJkK19LRHAFyFwClVeL +waZBQZx5AgMBAAECggEAA3yJYduVxsfLrxdG4PO/hDY2VPSCVj6j6XP4lDl6Rc3v +jBLQkq1WdC+KLQ9KjntWJLQwjcxj+CH7fQKfJSAkbOw24aqaKeOKTzVBssRilAt0 +MeRpg0WhYlhJHI0CSdQxOjUdFA8DDvkU2JJmCloPKkDVvqI/EzPWUDvRSsIJ9Uvp +p6hgaXijI2A73SyYB2CyNZB8xbr82hqxqsEKWKqeXvApKsgYuY1iwym6VfSdM3C1 +kyshj97bsyHF4V947y+GfJ15C5/Qy1Fftequ9l1/b9ArURHINV8wXVA0kt89vQDW +ALKrd5FDkk+dqlP4v0aIPJdA/+vRv044H+oWb9yS8QKBgQDrfmrtUfOK9SZFJP/K +iot7i/hALxB+5blEWU39dRt/aXkivgEX2MpZU+AaJEDAzLusQDG8mTUCi2Elspzw +hMbvrqfXIKNc+iAKicme82/M0K5R1J4F6aoY784OEFzfOzllPIVXkqzAQ5C35nha +nnPvYaum8E8gU7FB3q4+GE4ZpQKBgQC8UQQ/oVZv9wPZEHrsfH8az9Sor25cQDLh +uA57+UJMvUE2C+jff/zrHoNHSAN2u12DSjgDq7gZZIrQ5nJZa+74XgQsfRB0LMCa +13R1mqGKqrIkLfTUwrSu+zDOOgj56rAvcoGijUPlxA52STDiUG8jOXcYCwcsttrh +DX7c0NV3RQKBgBqv+egpKGNwAsVFOyO17bazlw+XBdSLriI7yKXXlqUqy8qPI8qT +C+NxqOztfNUcnowXvks0RZijOQAvrK9pLK5O6cBsd3b69ZScJtg3mEzqvUaSxNHA +uEEZNA3N9uhpasi5Qhc5jRBA1+6vVKZYXKUdOXmytBTixyzC0pZA5ODtAoGAJi9j +Zien6FI1RImT82oXN++WlpHkFe6qopx4y2iyqe7iTzxA/zjrpx1rz0Np+GRrxNiC +3TOvw+5gO5XO8BTzwwcFX6QIdxYwA/XWVpaBVSXhs3ZiI9ZLXwPuVJLuCQcurKBS +awysDOYhbJjQwcM/levsG0L5NHbrkSD0bYAnGOUCgYEArJdDxOQ/71NBJTAyrSG8 +T//hbohsUzLX2XX2xkRAnj/cPB2fLr/lBhg5golH/T8P7RmHVb5DNx5N5ZMbVCOw +MOIr4OyTq+TcdE8mmOFZ8TCIDqLHQeYhRl6zvaZG873NhC7Mn7GyCjl/QsUGW8fq +8gRSGDWq34jZYvPVfKfk1RY= +-----END PRIVATE KEY-----